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