Artifact modules moved in new hawkbit-artifact parent (#2012)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2024-11-12 09:40:44 +02:00
committed by GitHub
parent aa4c753ffe
commit 32acb44e31
36 changed files with 100 additions and 10 deletions

View File

@@ -0,0 +1,3 @@
# hawkBit Artifact API
Various internal interfaces artifact API classes.

View File

@@ -0,0 +1,46 @@
<!--
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-artifact-parent</artifactId>
<version>${revision}</version>
</parent>
<artifactId>hawkbit-artifact-api</artifactId>
<name>hawkBit :: Artifact Repository :: API</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
<!-- TEST -->
<dependency>
<groupId>io.github.classgraph</groupId>
<artifactId>classgraph</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,166 @@
/**
* Copyright (c) 2018 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.artifact.repository;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.springframework.util.ObjectUtils;
/**
* Abstract utility class for ArtifactRepository implementations with common
* functionality, e.g. computation of hashes.
*/
@Slf4j
public abstract class AbstractArtifactRepository implements ArtifactRepository {
private static final String TEMP_FILE_PREFIX = "tmp";
private static final String TEMP_FILE_SUFFIX = "artifactrepo";
@Override
// suppress warning, of not strong enough hashing algorithm, SHA-1 and MD5 is not used security related
@SuppressWarnings("squid:S2070")
public AbstractDbArtifact store(final String tenant,
final InputStream content, final String filename, final String contentType,
final DbArtifactHash providedHashes) {
final MessageDigest mdSHA1;
final MessageDigest mdMD5;
final MessageDigest mdSHA256;
try {
mdSHA1 = MessageDigest.getInstance("SHA1");
mdMD5 = MessageDigest.getInstance("MD5");
mdSHA256 = MessageDigest.getInstance("SHA-256");
} catch (final NoSuchAlgorithmException e) {
throw new ArtifactStoreException(e.getMessage(), e);
}
String tempFile = null;
try (final DigestInputStream inputStream = wrapInDigestInputStream(content, mdSHA1, mdMD5, mdSHA256)) {
tempFile = storeTempFile(inputStream);
final HexFormat hexFormat = HexFormat.of().withLowerCase();
final String sha1Hash16 = hexFormat.formatHex(mdSHA1.digest());
final String md5Hash16 = hexFormat.formatHex(mdMD5.digest());
final String sha256Hash16 = hexFormat.formatHex(mdSHA256.digest());
checkHashes(providedHashes, sha1Hash16, md5Hash16, sha256Hash16);
// Check if file with same sha1 hash exists and if so return it
if (existsByTenantAndSha1(tenant, sha1Hash16)) {
return addMissingHashes(getArtifactBySha1(tenant, sha1Hash16), sha1Hash16, md5Hash16, sha256Hash16);
}
return store(sanitizeTenant(tenant), new DbArtifactHash(sha1Hash16, md5Hash16, sha256Hash16), contentType, tempFile);
} catch (final IOException e) {
throw new ArtifactStoreException(e.getMessage(), e);
} finally {
if (!ObjectUtils.isEmpty(tempFile)) {
deleteTempFile(tempFile);
}
}
}
protected static String sanitizeTenant(final String tenant) {
return tenant.trim().toUpperCase();
}
protected void deleteTempFile(final String tempFile) {
final File file = new File(tempFile);
try {
Files.deleteIfExists(file.toPath());
} catch (final IOException e) {
log.error("Could not delete temp file {} ({})", file, e.getMessage());
}
}
protected String storeTempFile(final InputStream content) throws IOException {
final File file = createTempFile();
try (final OutputStream outputstream = new BufferedOutputStream(new FileOutputStream(file))) {
content.transferTo(outputstream);
outputstream.flush();
}
return file.getPath();
}
protected abstract AbstractDbArtifact store(final String tenant, final DbArtifactHash base16Hashes,
final String contentType, final String tempFile) throws IOException;
private static File createTempFile() {
try {
final File file = Files.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX).toFile();
if (!file.setReadable(true, true) ||
!file.setWritable(true, true)) {
throw new IOException("Can't set proper permissions!");
}
// try, if not supported - ok
file.setExecutable(false);
file.deleteOnExit();
return file;
} catch (final IOException e) {
throw new ArtifactStoreException("Cannot create temp file", e);
}
}
private static void checkHashes(final DbArtifactHash providedHashes,
final String sha1Hash16, final String md5Hash16, final String sha256Hash16) {
if (providedHashes == null) {
return;
}
if (areHashesNotMatching(providedHashes.getSha1(), sha1Hash16)) {
throw new HashNotMatchException("The given sha1 hash " + providedHashes.getSha1() +
" does not match the calculated sha1 hash " + sha1Hash16, HashNotMatchException.SHA1);
}
if (areHashesNotMatching(providedHashes.getMd5(), md5Hash16)) {
throw new HashNotMatchException("The given md5 hash " + providedHashes.getMd5() +
" does not match the calculated md5 hash " + md5Hash16, HashNotMatchException.MD5);
}
if (areHashesNotMatching(providedHashes.getSha256(), sha256Hash16)) {
throw new HashNotMatchException("The given sha256 hash " + providedHashes.getSha256() +
" does not match the calculated sha256 hash " + sha256Hash16, HashNotMatchException.SHA256);
}
}
private static boolean areHashesNotMatching(final String providedHashValue, final String hashValue) {
return providedHashValue != null && !hashValue.equals(providedHashValue);
}
private static DigestInputStream wrapInDigestInputStream(final InputStream input,
final MessageDigest mdSHA1, final MessageDigest mdMD5, final MessageDigest mdSHA256) {
return new DigestInputStream(new DigestInputStream(new DigestInputStream(input, mdSHA256), mdMD5), mdSHA1);
}
private AbstractDbArtifact addMissingHashes(final AbstractDbArtifact existing,
final String calculatedSha1, final String calculatedMd5, final String calculatedSha256) {
final String sha1 = checkEmpty(existing.getHashes().getSha1(), calculatedSha1);
final String md5 = checkEmpty(existing.getHashes().getMd5(), calculatedMd5);
final String sha256 = checkEmpty(existing.getHashes().getSha256(), calculatedSha256);
existing.setHashes(new DbArtifactHash(sha1, md5, sha256));
return existing;
}
private String checkEmpty(final String value, final String fallback) {
return ObjectUtils.isEmpty(value) ? fallback : value;
}
}

View File

@@ -0,0 +1,76 @@
/**
* 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.artifact.repository;
import java.io.InputStream;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
/**
* ArtifactRepository service interface.
*/
public interface ArtifactRepository {
/**
* Stores an artifact into the repository.
*
* @param tenant the tenant to store the artifact
* @param content the content to store
* @param filename the filename of the artifact
* @param contentType the content type of the artifact
* @param hash the hashes of the artifact to do hash-checks after storing the artifact, might be {@code null}
* @return the stored artifact
* @throws UnsupportedOperationException if implementation does not support the operation
* @throws ArtifactStoreException in case storing of the artifact was not successful
* @throws HashNotMatchException in case {@code hash} is provided and not matching to the calculated hashes during storing
*/
AbstractDbArtifact store(
@NotEmpty String tenant, @NotNull InputStream content, @NotEmpty String filename,
String contentType, DbArtifactHash hash);
/**
* Deletes an artifact by its SHA1 hash.
*
* @param tenant the tenant to store the artifact
* @param sha1Hash the sha1-hash of the artifact to delete
* @throws UnsupportedOperationException if implementation does not support the operation
*/
void deleteBySha1(@NotEmpty String tenant, @NotEmpty String sha1Hash);
/**
* Retrieves a {@link AbstractDbArtifact} from the store by its SHA1 hash.
*
* @param tenant the tenant to store the artifact
* @param sha1Hash the sha1-hash of the file to lookup.
* @return The artifact file object or {@code null} if no file exists.
* @throws UnsupportedOperationException if implementation does not support the operation
*/
AbstractDbArtifact getArtifactBySha1(@NotEmpty String tenant, @NotEmpty String sha1Hash);
/**
* Deletes all artifacts of given tenant.
*
* @param tenant to erase
*/
void deleteByTenant(@NotEmpty String tenant);
/**
* Checks if an artifact exists for a given tenant by its sha1 hash
*
* @param tenant the tenant
* @param sha1Hash the sha1-hash of the file to lookup.
* @return the boolean whether the artifact exists or not
*/
boolean existsByTenantAndSha1(@NotEmpty String tenant, @NotEmpty String sha1Hash);
}

View File

@@ -0,0 +1,31 @@
/**
* 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.artifact.repository;
import java.io.Serial;
/**
* {@link ArtifactStoreException} is thrown in case storing of an artifact was not successful.
*/
public class ArtifactStoreException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
/**
* Constructs a ArtifactStoreException with message and cause.
*
* @param message the message of the exception
* @param cause of the exception
*/
public ArtifactStoreException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,41 @@
/**
* 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.artifact.repository;
import java.io.Serial;
import lombok.Getter;
/**
* Thrown when provided hashes and hashes calculated during storing are not matching.
*/
public class HashNotMatchException extends RuntimeException {
public static final String SHA1 = "SHA-1";
public static final String MD5 = "MD5";
public static final String SHA256 = "SHA-256";
@Serial
private static final long serialVersionUID = 1L;
@Getter
private final String hashFunction;
/**
* Constructs a HashNotMatchException with message.
*
* @param message the message of the exception
* @param hashFunction the hash function which caused this exception
*/
public HashNotMatchException(final String message, final String hashFunction) {
super(message);
this.hashFunction = hashFunction;
}
}

View File

@@ -0,0 +1,36 @@
/**
* 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.artifact.repository.model;
import lombok.Data;
import org.springframework.util.Assert;
/**
* Database representation of artifact.
*/
@Data
public abstract class AbstractDbArtifact implements DbArtifact {
private final String artifactId;
private final long size;
private final String contentType;
private DbArtifactHash hashes;
protected AbstractDbArtifact(final String artifactId, final DbArtifactHash hashes, final long size,
final String contentType) {
Assert.notNull(artifactId, "Artifact ID cannot be null");
Assert.notNull(hashes, "Hashes cannot be null");
this.artifactId = artifactId;
this.hashes = hashes;
this.size = size;
this.contentType = contentType;
}
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2021 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.artifact.repository.model;
import java.io.InputStream;
/**
* Interface definition for artifact binary.
*/
public interface DbArtifact {
/**
* @return ID of the artifact
*/
String getArtifactId();
/**
* @return hashes of the artifact
*/
DbArtifactHash getHashes();
/**
* @return size of the artifact in bytes
*/
long getSize();
/**
* @return content-type if known by the repository or <code>null</code>
*/
String getContentType();
/**
* Creates an {@link InputStream} on this artifact. Caller has to take care of
* closing the stream. Repeatable calls open a new {@link InputStream}.
*
* @return {@link InputStream} to read from artifact.
*/
InputStream getFileInputStream();
}

View File

@@ -0,0 +1,36 @@
/**
* 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.artifact.repository.model;
import lombok.Data;
/**
* Database representation of artifact hash.
*/
@Data
public class DbArtifactHash {
private final String sha1;
private final String md5;
private final String sha256;
/**
* Constructor.
*
* @param sha1 the sha1 hash
* @param md5 the md5 hash
* @param sha256 the sha256 hash
*/
public DbArtifactHash(final String sha1, final String md5, final String sha256) {
this.sha1 = sha1;
this.md5 = md5;
this.sha256 = sha256;
}
}

View File

@@ -0,0 +1,31 @@
/**
* 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.artifact.repository.urlhandler;
/**
* hawkBit API type.
*/
public enum ApiType {
/**
* Support for Device Management Federation API.
*/
DMF,
/**
* Support for Direct Device Integration API.
*/
DDI,
/**
* Support for Management API.
*/
MGMT
}

View File

@@ -0,0 +1,36 @@
/**
* 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.artifact.repository.urlhandler;
import lombok.Data;
/**
* Container for a generated Artifact URL.
*/
@Data
public class ArtifactUrl {
private final String protocol;
private final String rel;
private final String ref;
/**
* Constructor.
*
* @param protocol string, e.g. ftp, http, https
* @param rel hypermedia value
* @param ref hypermedia value
*/
public ArtifactUrl(final String protocol, final String rel, final String ref) {
this.protocol = protocol;
this.rel = rel;
this.ref = ref;
}
}

View File

@@ -0,0 +1,39 @@
/**
* 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.artifact.repository.urlhandler;
import java.net.URI;
import java.util.List;
/**
* Interface declaration of the {@link ArtifactUrlHandler} which generates the URLs to specific artifacts.
*/
public interface ArtifactUrlHandler {
/**
* Returns a generated download URL for a given artifact parameters for a specific protocol.
*
* @param placeholder data for URL generation
* @param api given protocol that URL needs to support
* @return a URL for the given artifact parameters in a given protocol
*/
List<ArtifactUrl> getUrls(URLPlaceholder placeholder, ApiType api);
/**
* Returns a generated download URL for a given artifact parameters for a
* specific protocol.
*
* @param placeholder data for URL generation
* @param api given protocol that URL needs to support
* @param requestUri of the request that allows the handler to align the generated URL to the original request.
* @return a URL for the given artifact parameters in a given protocol
*/
List<ArtifactUrl> getUrls(URLPlaceholder placeholder, ApiType api, URI requestUri);
}

View File

@@ -0,0 +1,91 @@
/**
* 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.artifact.repository.urlhandler;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Artifact handler properties class for holding all supported protocols with
* host, ip, port and download pattern.
*
* @see PropertyBasedArtifactUrlHandler
*/
@Data
@ConfigurationProperties("hawkbit.artifact.url")
public class ArtifactUrlHandlerProperties {
/**
* Rel as key and complete protocol as value.
*/
private final Map<String, UrlProtocol> protocols = new HashMap<>();
/**
* Protocol specific properties to generate URLs accordingly.
*/
@Data
public static class UrlProtocol {
private static final int DEFAULT_HTTP_PORT = 8080;
/**
* Set to true if enabled.
*/
private boolean enabled = true;
/**
* Hypermedia rel value for this protocol.
*/
private String rel = "download-http";
/**
* Hypermedia ref pattern for this protocol. Supported placeholders are the properties
* supported by {@link PropertyBasedArtifactUrlHandler}.
*/
private String ref = PropertyBasedArtifactUrlHandler.DEFAULT_URL_PROTOCOL_REF;
/**
* Protocol name placeholder that can be used in ref pattern.
*/
private String protocol = "http";
/**
* Hostname placeholder that can be used in ref pattern.
*/
private String hostname = "localhost";
/**
* IP address placeholder that can be used in ref pattern.
*/
// Exception squid:S1313 - default only, can be configured
@SuppressWarnings("squid:S1313")
private String ip = "127.0.0.1";
/**
* Port placeholder that can be used in ref pattern.
*/
private Integer port = DEFAULT_HTTP_PORT;
/**
* Support for the following hawkBit API.
*/
private List<ApiType> supports = Arrays.asList(ApiType.DDI, ApiType.DMF, ApiType.MGMT);
public void setSupports(final List<ApiType> supports) {
this.supports = Collections.unmodifiableList(supports);
}
}
}

View File

@@ -0,0 +1,67 @@
/**
* 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.artifact.repository.urlhandler;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* Utility class for Base10 to Base62 conversion and vice versa. Base62 has the benefit of being shorter in ASCII representation than Base10.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
final class Base62Util {
private static final String BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private static final int BASE62_BASE = BASE62_ALPHABET.length();
/**
* @param l number
* @return converted number into Base62 ASCII string
*/
static String fromBase10(final long l) {
if (l == 0) {
return "0";
}
long temp = l;
final StringBuilder sb = new StringBuilder();
while (temp > 0) {
temp = fromBase10(temp, sb);
}
return sb.reverse().toString();
}
/**
* @param base62 number
* @return converted number into Base10
*/
static Long toBase10(final String base62) {
return toBase10(new StringBuilder(base62).reverse().toString().toCharArray());
}
static Long fromBase10(final long l, final StringBuilder sb) {
final int rem = (int) (l % BASE62_BASE);
sb.append(BASE62_ALPHABET.charAt(rem));
return l / BASE62_BASE;
}
private static Long toBase10(final char[] chars) {
long base10 = 0L;
for (int i = chars.length - 1; i >= 0; i--) {
base10 += toBase10(BASE62_ALPHABET.indexOf(chars[i]), i);
}
return base10;
}
private static int toBase10(final int n, final int pow) {
return n * (int) Math.pow(BASE62_BASE, pow);
}
}

View File

@@ -0,0 +1,203 @@
/**
* 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.artifact.repository.urlhandler;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.artifact.repository.urlhandler.ArtifactUrlHandlerProperties.UrlProtocol;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Implementation for ArtifactUrlHandler for creating urls to download resource based on patterns configured by
* {@link ArtifactUrlHandlerProperties}.
*
* This mechanism can be used to generate links to arbitrary file hosting infrastructure. However, the hawkBit update server
* supports hosting files as well in the following {@link UrlProtocol#getRef()} patterns:
*
* Default:
* {protocol}://{hostname}:{port}{contextPath}/{tenant}/controller/v1/{controllerId}
* /softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}
*
* Default (MD5SUM files):
* {protocol}://{hostname}:{port}{contextPath}/{tenant}/controller/v1/{controllerId}/
* softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}.MD5SUM
*/
public class PropertyBasedArtifactUrlHandler implements ArtifactUrlHandler {
private static final String PROTOCOL_PLACEHOLDER = "protocol";
private static final String HOSTNAME_PLACEHOLDER = "hostname";
private static final String IP_PLACEHOLDER = "ip";
private static final String PORT_PLACEHOLDER = "port";
private static final String CONTEXT_PATH = "contextPath";
private static final String CONTROLLER_ID_PLACEHOLDER = "controllerId";
private static final String TARGET_ID_BASE10_PLACEHOLDER = "targetId";
private static final String TARGET_ID_BASE62_PLACEHOLDER = "targetIdBase62";
private static final String HOSTNAME_REQUEST_PLACEHOLDER = "hostnameRequest";
private static final String PORT_REQUEST_PLACEHOLDER = "portRequest";
private static final String PROTOCOL_REQUEST_PLACEHOLDER = "protocolRequest";
private static final String HOSTNAME_WITH_DOMAIN_REQUEST_PLACEHOLDER = "domainRequest";
private static final String ARTIFACT_FILENAME_PLACEHOLDER = "artifactFileName";
private static final String ARTIFACT_SHA1_PLACEHOLDER = "artifactSHA1";
private static final String ARTIFACT_ID_BASE10_PLACEHOLDER = "artifactId";
private static final String ARTIFACT_ID_BASE62_PLACEHOLDER = "artifactIdBase62";
private static final String TENANT_PLACEHOLDER = "tenant";
private static final String TENANT_ID_BASE10_PLACEHOLDER = "tenantId";
private static final String TENANT_ID_BASE62_PLACEHOLDER = "tenantIdBase62";
private static final String SOFTWARE_MODULE_ID_BASE10_PLACEHOLDER = "softwareModuleId";
final static String DEFAULT_URL_PROTOCOL_REF = "{" + PROTOCOL_PLACEHOLDER + "}://{" + HOSTNAME_PLACEHOLDER + "}:{" + PORT_PLACEHOLDER + "}{" + CONTEXT_PATH + "}/{" + TENANT_PLACEHOLDER + "}/controller/v1/{" + CONTROLLER_ID_PLACEHOLDER + "}/softwaremodules/{" + SOFTWARE_MODULE_ID_BASE10_PLACEHOLDER + "}/artifacts/{" + ARTIFACT_FILENAME_PLACEHOLDER + "}";
private static final String SOFTWARE_MODULE_ID_BASE62_PLACEHOLDER = "softwareModuleIdBase62";
private final ArtifactUrlHandlerProperties urlHandlerProperties;
private final String contextPath;
/**
* @param urlHandlerProperties for URL generation configuration
*/
public PropertyBasedArtifactUrlHandler(final ArtifactUrlHandlerProperties urlHandlerProperties, final String contextPath) {
this.urlHandlerProperties = urlHandlerProperties;
this.contextPath = contextPath == null || "/".equals(contextPath) ? "" : contextPath; // normalize
}
@Override
public List<ArtifactUrl> getUrls(final URLPlaceholder placeholder, final ApiType api) {
return getUrls(placeholder, api, null);
}
@Override
public List<ArtifactUrl> getUrls(final URLPlaceholder placeholder, final ApiType api, final URI requestUri) {
return urlHandlerProperties.getProtocols().values().stream()
.filter(urlProtocol -> urlProtocol.getSupports().contains(api) && urlProtocol.isEnabled())
.map(urlProtocol -> new ArtifactUrl(urlProtocol.getProtocol().toUpperCase(), urlProtocol.getRel(),
generateUrl(urlProtocol, placeholder, requestUri)))
.collect(Collectors.toList());
}
private static String getRequestPort(final UrlProtocol protocol, final URI requestUri) {
if (requestUri == null) {
return getPort(protocol);
}
// if port undefined then default protocol port is used
return requestUri.getPort() > 0 ? String.valueOf(requestUri.getPort()) : "";
}
private static String getRequestHost(final UrlProtocol protocol, final URI requestUri) {
if (requestUri == null) {
return protocol.getHostname();
}
return Optional.ofNullable(requestUri.getHost()).orElse(protocol.getHostname());
}
private static String getRequestProtocol(final UrlProtocol protocol, final URI requestUri) {
if (requestUri == null) {
return protocol.getProtocol();
}
return Optional.ofNullable(requestUri.getScheme()).orElse(protocol.getProtocol());
}
private static String getPort(final UrlProtocol protocol) {
return ObjectUtils.isEmpty(protocol.getPort()) ? null : String.valueOf(protocol.getPort());
}
private static String computeHostWithRequestDomain(final UrlProtocol protocol, final URI requestUri) {
if (requestUri == null) {
return protocol.getHostname();
}
if (!protocol.getHostname().contains(".")) {
return protocol.getHostname();
}
final String host = StringUtils.delimitedListToStringArray(protocol.getHostname(), ".")[0].trim();
final List<String> domainElements = Arrays
.asList(StringUtils.delimitedListToStringArray(requestUri.getHost(), "."));
final String domain = StringUtils.collectionToDelimitedString(domainElements.subList(1, domainElements.size()),
".");
if (ObjectUtils.isEmpty(domain)) {
return protocol.getHostname();
}
return host + "." + domain;
}
private String generateUrl(final UrlProtocol protocol, final URLPlaceholder placeholder,
final URI requestUri) {
final Set<Entry<String, String>> entrySet = getReplaceMap(protocol, placeholder, requestUri).entrySet();
String urlPattern = protocol.getRef();
for (final Entry<String, String> entry : entrySet) {
if (List.of(PORT_PLACEHOLDER, PORT_REQUEST_PLACEHOLDER).contains(entry.getKey())) {
urlPattern = urlPattern.replace(":{" + entry.getKey() + "}",
ObjectUtils.isEmpty(entry.getValue()) ? "" : (":" + entry.getValue()));
} else {
if (entry.getValue() != null) {
urlPattern = urlPattern.replace("{" + entry.getKey() + "}", entry.getValue());
}
}
}
return urlPattern;
}
private Map<String, String> getReplaceMap(final UrlProtocol protocol, final URLPlaceholder placeholder,
final URI requestUri) {
final Map<String, String> replaceMap = new HashMap<>();
replaceMap.put(IP_PLACEHOLDER, protocol.getIp());
replaceMap.put(HOSTNAME_PLACEHOLDER, protocol.getHostname());
replaceMap.put(HOSTNAME_REQUEST_PLACEHOLDER, getRequestHost(protocol, requestUri));
replaceMap.put(PORT_REQUEST_PLACEHOLDER, getRequestPort(protocol, requestUri));
replaceMap.put(HOSTNAME_WITH_DOMAIN_REQUEST_PLACEHOLDER, computeHostWithRequestDomain(protocol, requestUri));
replaceMap.put(PROTOCOL_REQUEST_PLACEHOLDER, getRequestProtocol(protocol, requestUri));
replaceMap.put(CONTEXT_PATH, contextPath);
replaceMap.put(ARTIFACT_FILENAME_PLACEHOLDER,
URLEncoder.encode(placeholder.getSoftwareData().getFilename(), StandardCharsets.UTF_8));
replaceMap.put(ARTIFACT_SHA1_PLACEHOLDER, placeholder.getSoftwareData().getSha1Hash());
replaceMap.put(PROTOCOL_PLACEHOLDER, protocol.getProtocol());
replaceMap.put(PORT_PLACEHOLDER, getPort(protocol));
replaceMap.put(TENANT_PLACEHOLDER, placeholder.getTenant());
replaceMap.put(TENANT_ID_BASE10_PLACEHOLDER, String.valueOf(placeholder.getTenantId()));
replaceMap.put(TENANT_ID_BASE62_PLACEHOLDER, Base62Util.fromBase10(placeholder.getTenantId()));
replaceMap.put(CONTROLLER_ID_PLACEHOLDER, placeholder.getControllerId());
replaceMap.put(TARGET_ID_BASE10_PLACEHOLDER, String.valueOf(placeholder.getTargetId()));
if (placeholder.getTargetId() != null) {
replaceMap.put(TARGET_ID_BASE62_PLACEHOLDER, Base62Util.fromBase10(placeholder.getTargetId()));
}
replaceMap.put(ARTIFACT_ID_BASE62_PLACEHOLDER,
Base62Util.fromBase10(placeholder.getSoftwareData().getArtifactId()));
replaceMap.put(ARTIFACT_ID_BASE10_PLACEHOLDER, String.valueOf(placeholder.getSoftwareData().getArtifactId()));
replaceMap.put(SOFTWARE_MODULE_ID_BASE10_PLACEHOLDER,
String.valueOf(placeholder.getSoftwareData().getSoftwareModuleId()));
replaceMap.put(SOFTWARE_MODULE_ID_BASE62_PLACEHOLDER,
Base62Util.fromBase10(placeholder.getSoftwareData().getSoftwareModuleId()));
return replaceMap;
}
}

View File

@@ -0,0 +1,71 @@
/**
* 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.artifact.repository.urlhandler;
import lombok.Data;
/**
* Container for variables available to the {@link ArtifactUrlHandler}.
*/
@Data
public class URLPlaceholder {
private final String tenant;
private final Long tenantId;
private final String controllerId;
private final Long targetId;
private final SoftwareData softwareData;
/**
* Constructor.
*
* @param tenant of the client
* @param tenantId of teh tenant
* @param controllerId of the target
* @param targetId of the target
* @param softwareData information about the artifact and software module that can be accessed by the URL.
*/
public URLPlaceholder(final String tenant, final Long tenantId, final String controllerId, final Long targetId,
final SoftwareData softwareData) {
this.tenant = tenant;
this.tenantId = tenantId;
this.controllerId = controllerId;
this.targetId = targetId;
this.softwareData = softwareData;
}
/**
* Information about the artifact and software module that can be accessed
* by the URL.
*/
@Data
public static class SoftwareData {
private Long softwareModuleId;
private String filename;
private Long artifactId;
private String sha1Hash;
/**
* Constructor.
*
* @param softwareModuleId of the module the artifact belongs to
* @param filename of the artifact
* @param artifactId of the artifact
* @param sha1Hash of the artifact
*/
public SoftwareData(final Long softwareModuleId, final String filename, final Long artifactId, final String sha1Hash) {
this.softwareModuleId = softwareModuleId;
this.filename = filename;
this.artifactId = artifactId;
this.sha1Hash = sha1Hash;
}
}
}

View File

@@ -0,0 +1,40 @@
/**
* 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.artifact.repository.urlhandler;
import static org.assertj.core.api.Assertions.assertThat;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
@Feature("Unit Tests - Artifact URL Handler")
@Story("Base62 Utility tests")
class Base62UtilTest {
@Test
@Description("Convert Base10 numbres to Base62 ASCII strings.")
void fromBase10() {
assertThat(Base62Util.fromBase10(0L)).isEqualTo("0");
assertThat(Base62Util.fromBase10(11L)).isEqualTo("B");
assertThat(Base62Util.fromBase10(36L)).isEqualTo("a");
assertThat(Base62Util.fromBase10(999L)).isEqualTo("G7");
}
@Test
@Description("Convert Base62 ASCII strings to Base10 numbers.")
void toBase10() {
assertThat(Base62Util.toBase10("0")).isZero();
assertThat(Base62Util.toBase10("B")).isEqualTo(11);
assertThat(Base62Util.toBase10("a")).isEqualTo(36L);
assertThat(Base62Util.toBase10("G7")).isEqualTo(999L);
}
}

View File

@@ -0,0 +1,226 @@
/**
* 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.artifact.repository.urlhandler;
import static org.assertj.core.api.Assertions.assertThat;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.eclipse.hawkbit.artifact.repository.urlhandler.ApiType;
import org.eclipse.hawkbit.artifact.repository.urlhandler.ArtifactUrl;
import org.eclipse.hawkbit.artifact.repository.urlhandler.ArtifactUrlHandler;
import org.eclipse.hawkbit.artifact.repository.urlhandler.ArtifactUrlHandlerProperties;
import org.eclipse.hawkbit.artifact.repository.urlhandler.ArtifactUrlHandlerProperties.UrlProtocol;
import org.eclipse.hawkbit.artifact.repository.urlhandler.PropertyBasedArtifactUrlHandler;
import org.eclipse.hawkbit.artifact.repository.urlhandler.URLPlaceholder;
import org.eclipse.hawkbit.artifact.repository.urlhandler.URLPlaceholder.SoftwareData;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
/**
* Tests for creating urls to download artifacts.
*/
@Feature("Unit Tests - Artifact URL Handler")
@Story("Test to generate the artifact download URL")
@ExtendWith(MockitoExtension.class)
public class PropertyBasedArtifactUrlHandlerTest {
private static final String TEST_PROTO = "coap";
private static final String TEST_REL = "download-udp";
private static final long TENANT_ID = 456789L;
private static final String CONTROLLER_ID = "Test";
private static final String FILENAME_DECODE = "test123!§$%&";
private static final String FILENAME_ENCODE = "test123%21%C2%A7%24%25%26";
private static final long SOFTWAREMODULEID = 87654L;
private static final long TARGETID = 3474366L;
private static final String TARGETID_BASE62 = "EZqA";
private static final String SHA1HASH = "test12345";
private static final long ARTIFACTID = 1345678L;
private static final String ARTIFACTID_BASE62 = "5e4U";
private static final String TENANT = "TEST_TENANT";
private static final String HTTP_LOCALHOST = "http://localhost:8080/";
private static URLPlaceholder placeholder = new URLPlaceholder(TENANT, TENANT_ID, CONTROLLER_ID, TARGETID,
new SoftwareData(SOFTWAREMODULEID, FILENAME_DECODE, ARTIFACTID, SHA1HASH));
private ArtifactUrlHandler urlHandlerUnderTest;
private ArtifactUrlHandlerProperties properties;
@BeforeEach
public void setup() {
properties = new ArtifactUrlHandlerProperties();
urlHandlerUnderTest = new PropertyBasedArtifactUrlHandler(properties, "");
}
@Test
@Description("Tests the generation of http download url.")
public void urlGenerationWithDefaultConfiguration() {
properties.getProtocols().put("download-http", new UrlProtocol());
final List<ArtifactUrl> ddiUrls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DDI);
assertThat(ddiUrls).containsExactly(
new ArtifactUrl("http".toUpperCase(), "download-http", HTTP_LOCALHOST + TENANT + "/controller/v1/"
+ CONTROLLER_ID + "/softwaremodules/" + SOFTWAREMODULEID + "/artifacts/" + FILENAME_ENCODE));
final List<ArtifactUrl> dmfUrls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DMF);
assertThat(ddiUrls).isEqualTo(dmfUrls);
}
@Test
@Description("Tests the generation of custom download url with a CoAP example that supports DMF only.")
public void urlGenerationWithCustomConfiguration() {
final UrlProtocol proto = new UrlProtocol();
proto.setIp("127.0.0.1");
proto.setPort(5683);
proto.setProtocol(TEST_PROTO);
proto.setRel(TEST_REL);
proto.setSupports(List.of(ApiType.DMF));
proto.setRef("{protocol}://{ip}:{port}/fw/{tenant}/{controllerId}/sha1/{artifactSHA1}");
properties.getProtocols().put(TEST_PROTO, proto);
List<ArtifactUrl> urls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DDI);
assertThat(urls).isEmpty();
urls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DMF);
assertThat(urls).containsExactly(new ArtifactUrl(TEST_PROTO.toUpperCase(), TEST_REL,
"coap://127.0.0.1:5683/fw/" + TENANT + "/" + CONTROLLER_ID + "/sha1/" + SHA1HASH));
}
@Test
@Description("Tests the generation of custom download url using Base62 references with a CoAP example that supports DMF only.")
public void urlGenerationWithCustomShortConfiguration() {
final UrlProtocol proto = new UrlProtocol();
proto.setIp("127.0.0.1");
proto.setPort(5683);
proto.setProtocol(TEST_PROTO);
proto.setRel(TEST_REL);
proto.setSupports(List.of(ApiType.DMF));
proto.setRef("{protocol}://{ip}:{port}/fws/{tenant}/{targetIdBase62}/{artifactIdBase62}");
properties.getProtocols().put("ftp", proto);
List<ArtifactUrl> urls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DDI);
assertThat(urls).isEmpty();
urls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DMF);
assertThat(urls).containsExactly(new ArtifactUrl(TEST_PROTO.toUpperCase(), TEST_REL,
TEST_PROTO + "://127.0.0.1:5683/fws/" + TENANT + "/" + TARGETID_BASE62 + "/" + ARTIFACTID_BASE62));
}
@Test
@Description("Verfies that the full qualified host of the statically defined hostname is replaced with the host of the request.")
public void urlGenerationWithHostFromRequest() throws URISyntaxException {
final String testHost = "ddi.host.com";
final UrlProtocol proto = new UrlProtocol();
proto.setIp("127.0.0.1");
proto.setPort(5683);
proto.setProtocol(TEST_PROTO);
proto.setRel(TEST_REL);
proto.setSupports(List.of(ApiType.DDI));
proto.setRef("{protocol}://{hostnameRequest}:{port}/fws/{tenant}/{targetIdBase62}/{artifactIdBase62}");
properties.getProtocols().put("ftp", proto);
final List<ArtifactUrl> urls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DDI,
new URI("https://" + testHost));
assertThat(urls).containsExactly(new ArtifactUrl(TEST_PROTO.toUpperCase(), TEST_REL, TEST_PROTO + "://"
+ testHost + ":5683/fws/" + TENANT + "/" + TARGETID_BASE62 + "/" + ARTIFACTID_BASE62));
}
@Test
@Description("Verfies that the protocol of the statically defined hostname is replaced with the protocol of the request.")
public void urlGenerationWithProtocolFromRequest() throws URISyntaxException {
final String testHost = "ddi.host.com";
final UrlProtocol proto = new UrlProtocol();
proto.setRef("{protocolRequest}://{hostname}:{port}/fws/{tenant}/{targetIdBase62}/{artifactIdBase62}");
properties.getProtocols().put("download-http", proto);
final List<ArtifactUrl> urls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DDI,
new URI("https://" + testHost));
assertThat(urls).containsExactly(new ArtifactUrl("http".toUpperCase(), "download-http",
"https://localhost:8080/fws/" + TENANT + "/" + TARGETID_BASE62 + "/" + ARTIFACTID_BASE62));
}
@Test
@Description("Verfies that the port of the statically defined hostname is replaced with the port of the request.")
public void urlGenerationWithPortFromRequest() throws URISyntaxException {
final UrlProtocol proto = new UrlProtocol();
proto.setRef(
"{protocol}://{hostname}:{portRequest}/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}");
properties.getProtocols().put("download-http", proto);
final List<ArtifactUrl> ddiUrls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DDI,
new URI("http://anotherHost.com:8083"));
assertThat(ddiUrls).containsExactly(new ArtifactUrl("http".toUpperCase(), "download-http",
"http://localhost:8083/" + TENANT + "/controller/v1/" + CONTROLLER_ID + "/softwaremodules/"
+ SOFTWAREMODULEID + "/artifacts/" + FILENAME_ENCODE));
final List<ArtifactUrl> dmfUrls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DMF);
assertThat(dmfUrls).containsExactly(new ArtifactUrl("http".toUpperCase(), "download-http",
"http://localhost:8080/" + TENANT + "/controller/v1/" + CONTROLLER_ID + "/softwaremodules/"
+ SOFTWAREMODULEID + "/artifacts/" + FILENAME_ENCODE));
}
@Test
@Description("Verfies that if default protocol port in request is used then url is returned without port")
public void urlGenerationWithPortFromRequestForHttps() throws URISyntaxException {
String protocol = "https";
final UrlProtocol proto = new UrlProtocol();
proto.setRef(
"{protocolRequest}://{hostnameRequest}:{portRequest}/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}");
proto.setProtocol(protocol);
properties.getProtocols().put("download-http", proto);
URI uri = new URI(protocol + "://anotherHost.com");
final List<ArtifactUrl> urls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DDI, uri);
assertThat(urls).containsExactly(new ArtifactUrl(protocol.toUpperCase(), "download-http",
uri + "/" + TENANT + "/controller/v1/" + CONTROLLER_ID + "/softwaremodules/"
+ SOFTWAREMODULEID + "/artifacts/" + FILENAME_ENCODE));
}
@Test
@Description("Verfies that the domain of the statically defined hostname is replaced with the domain of the request.")
public void urlGenerationWithDomainFromRequest() throws URISyntaxException {
final UrlProtocol proto = new UrlProtocol();
proto.setHostname("host.bumlux.net");
proto.setRef(
"{protocol}://{domainRequest}/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}");
properties.getProtocols().put("download-http", proto);
final List<ArtifactUrl> ddiUrls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DDI,
new URI("http://anotherHost.com:8083"));
assertThat(ddiUrls).containsExactly(
new ArtifactUrl("http".toUpperCase(), "download-http", "http://host.com/" + TENANT + "/controller/v1/"
+ CONTROLLER_ID + "/softwaremodules/" + SOFTWAREMODULEID + "/artifacts/" + FILENAME_ENCODE));
final List<ArtifactUrl> dmfUrls = urlHandlerUnderTest.getUrls(placeholder, ApiType.DMF);
assertThat(dmfUrls).containsExactly(new ArtifactUrl("http".toUpperCase(), "download-http",
"http://host.bumlux.net/" + TENANT + "/controller/v1/" + CONTROLLER_ID + "/softwaremodules/"
+ SOFTWAREMODULEID + "/artifacts/" + FILENAME_ENCODE));
}
}

View File

@@ -0,0 +1,87 @@
/**
* 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.artifact.repository.urlhandler;
import static org.assertj.core.api.Assertions.assertThat;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.eclipse.hawkbit.artifact.repository.urlhandler.URLPlaceholder;
import org.junit.jupiter.api.Test;
@Feature("Unit Tests - Artifact URL Handler")
@Story("URL placeholder tests")
class URLPlaceholderTest {
private final URLPlaceholder.SoftwareData softwareData;
private final URLPlaceholder placeholder;
public URLPlaceholderTest() {
this.softwareData = new URLPlaceholder.SoftwareData(1L, "file.txt", 123L, "someHash123");
this.placeholder = new URLPlaceholder("SuperCorp", 123L, "Super-1", 1L, softwareData);
}
@Test
@Description("Same object should be equal")
// Exception squid:S5785 - JUnit assertTrue/assertFalse should be simplified to
// the corresponding dedicated assertion
// Need to test the equals method and need to bypass magic logic in utility
// classes
@SuppressWarnings({ "squid:S5838" })
void sameObjectShouldBeEqual() {
assertThat(softwareData.equals(softwareData)).isTrue();
assertThat(placeholder.equals(placeholder)).isTrue();
}
@Test
@Description("Different object should not be equal")
@SuppressWarnings({ "squid:S5838" })
void differentObjectShouldNotBeEqual() {
final URLPlaceholder.SoftwareData softwareData2 = new URLPlaceholder.SoftwareData(2L, "file.txt", 123L, "someHash123");
final URLPlaceholder placeholder2 = new URLPlaceholder("SuperCorp", 123L, "Super-2", 2L, softwareData2);
final URLPlaceholder placeholderWithOtherSoftwareData = new URLPlaceholder(placeholder.getTenant(),
placeholder.getTenantId(), placeholder.getControllerId(), placeholder.getTargetId(), softwareData2);
assertThat(placeholder.equals(placeholder2)).isFalse();
assertThat(placeholder2.equals(placeholder)).isFalse();
assertThat(softwareData.equals(softwareData2)).isFalse();
assertThat(softwareData2.equals(softwareData)).isFalse();
assertThat(placeholder.equals(placeholderWithOtherSoftwareData)).isFalse();
}
@Test
@Description("Different objects with same properties should be equal")
void differentObjectsWithSamePropertiesShouldBeEqual() {
final URLPlaceholder placeholderWithSameProperties = new URLPlaceholder(placeholder.getTenant(), placeholder.getTenantId(),
placeholder.getControllerId(), placeholder.getTargetId(), softwareData);
assertThat(placeholder).isEqualTo(placeholderWithSameProperties);
assertThat(placeholderWithSameProperties).isEqualTo(placeholder);
}
@Test
@Description("Should not equal null")
// Exception squid:S5785 - JUnit assertTrue/assertFalse should be simplified to
// the corresponding dedicated assertion
// Need to test the equals method and need to bypass magic logic in utility
// classes
@SuppressWarnings({ "squid:S5838" })
void shouldNotEqualNull() {
assertThat(placeholder.equals(null)).isFalse();
assertThat(softwareData.equals(null)).isFalse();
}
@Test
@Description("HashCode should not change")
void hashCodeShouldNotChange() {
final URLPlaceholder placeholderWithSameProperties = new URLPlaceholder(placeholder.getTenant(), placeholder.getTenantId(),
placeholder.getControllerId(), placeholder.getTargetId(), softwareData);
assertThat(placeholder).hasSameHashCodeAs(placeholder).hasSameHashCodeAs(placeholderWithSameProperties);
}
}