SDK: Fix link download release (#2273)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-02-13 09:52:49 +02:00
committed by GitHub
parent 64ffc6a27a
commit 1c9153f080
2 changed files with 93 additions and 43 deletions

View File

@@ -289,39 +289,41 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
final MgmtActionConfirmationRequestBodyPut actionConfirmation) { final MgmtActionConfirmationRequestBodyPut actionConfirmation) {
log.debug("updateActionConfirmation with data [targetId={}, actionId={}]: {}", targetId, actionId, actionConfirmation); log.debug("updateActionConfirmation with data [targetId={}, actionId={}]: {}", targetId, actionId, actionConfirmation);
return getValidatedAction(targetId, actionId).map(action -> { return getValidatedAction(targetId, actionId)
try { .map(action -> {
switch (actionConfirmation.getConfirmation()) { try {
case CONFIRMED: switch (actionConfirmation.getConfirmation()) {
log.info("Confirmed the action (actionId: {}, targetId: {}) as we got {} report", case CONFIRMED:
actionId, targetId, actionConfirmation.getConfirmation()); log.debug("Confirmed the action (actionId: {}, targetId: {}) as we got {} report",
confirmationManagement.confirmAction(actionId, actionConfirmation.getCode(), actionConfirmation.getDetails()); actionId, targetId, actionConfirmation.getConfirmation());
break; confirmationManagement.confirmAction(actionId, actionConfirmation.getCode(), actionConfirmation.getDetails());
case DENIED: break;
default: case DENIED:
log.debug("Controller denied the action (actionId: {}, controllerId: {}) as we got {} report.", default:
actionId, targetId, actionConfirmation.getConfirmation()); log.debug("Controller denied the action (actionId: {}, controllerId: {}) as we got {} report.",
confirmationManagement.denyAction(actionId, actionConfirmation.getCode(), actionConfirmation.getDetails()); actionId, targetId, actionConfirmation.getConfirmation());
break; confirmationManagement.denyAction(actionId, actionConfirmation.getCode(), actionConfirmation.getDetails());
} break;
return new ResponseEntity<Void>(HttpStatus.OK); }
} catch (final InvalidConfirmationFeedbackException e) { return new ResponseEntity<Void>(HttpStatus.OK);
if (e.getReason() == InvalidConfirmationFeedbackException.Reason.ACTION_CLOSED) { } catch (final InvalidConfirmationFeedbackException e) {
log.warn("Updating action {} with confirmation {} not possible since action not active anymore.", if (e.getReason() == InvalidConfirmationFeedbackException.Reason.ACTION_CLOSED) {
action.getId(), actionConfirmation.getConfirmation(), e); log.warn("Updating action {} with confirmation {} not possible since action not active anymore.",
return new ResponseEntity<Void>(HttpStatus.GONE); action.getId(), actionConfirmation.getConfirmation(), e);
} else if (e.getReason() == InvalidConfirmationFeedbackException.Reason.NOT_AWAITING_CONFIRMATION) { return new ResponseEntity<Void>(HttpStatus.GONE);
log.debug("Action is not waiting for confirmation, deny request.", e); } else if (e.getReason() == InvalidConfirmationFeedbackException.Reason.NOT_AWAITING_CONFIRMATION) {
return new ResponseEntity<Void>(HttpStatus.NOT_FOUND); log.debug("Action is not waiting for confirmation, deny request.", e);
} else { return new ResponseEntity<Void>(HttpStatus.NOT_FOUND);
log.debug("Action confirmation failed with unknown reason.", e); } else {
return new ResponseEntity<Void>(HttpStatus.BAD_REQUEST); log.debug("Action confirmation failed with unknown reason.", e);
} return new ResponseEntity<Void>(HttpStatus.BAD_REQUEST);
} }
}).orElseGet(() -> { }
log.warn("Action {} not found for target {}", actionId, targetId); })
return ResponseEntity.notFound().build(); .orElseGet(() -> {
}); log.warn("Action {} not found for target {}", actionId, targetId);
return ResponseEntity.notFound().build();
});
} }
@Override @Override

View File

@@ -10,6 +10,7 @@
package org.eclipse.hawkbit.sdk; package org.eclipse.hawkbit.sdk;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@@ -38,6 +39,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Random; import java.util.Random;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -66,6 +68,7 @@ import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.TrustAllStrategy; import org.apache.hc.client5.http.ssl.TrustAllStrategy;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.ssl.SSLContextBuilder; import org.apache.hc.core5.ssl.SSLContextBuilder;
import org.springframework.hateoas.Link; import org.springframework.hateoas.Link;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -146,11 +149,17 @@ public class HawkbitClient {
return service(serviceType, tenantProperties, controller); return service(serviceType, tenantProperties, controller);
} }
/**
* Downloads a link. If the returned type (linkType) is {@link ClassicHttpResponse} or {@link InputStream} then the caller is responsible
* to close the response. Otherwise, it is assumed and json object, it is deserialized and returned.
*/
@SuppressWarnings("unchecked")
public static <T> T getLink(final Link link, final Class<T> linkType, final Tenant tenant, final Controller controller) throws IOException { public static <T> T getLink(final Link link, final Class<T> linkType, final Tenant tenant, final Controller controller) throws IOException {
final String url = link.getHref(); final String url = link.getHref();
final HttpClientKey key = new HttpClientKey( final HttpClientKey key = new HttpClientKey(
url.startsWith("https://"), controller == null ? null : controller.getCertificate(), tenant.getTenantCA()); url.startsWith("https://"), controller == null ? null : controller.getCertificate(), tenant.getTenantCA());
final HttpClient httpClient = httpClient(key); final HttpClient httpClient = httpClient(key);
final AtomicBoolean delegatedRelease = new AtomicBoolean(false);
try { try {
final HttpGet request = new HttpGet(url); final HttpGet request = new HttpGet(url);
final String gatewayToken = tenant.getGatewayToken(); final String gatewayToken = tenant.getGatewayToken();
@@ -168,17 +177,54 @@ public class HawkbitClient {
throw new IllegalStateException("Unexpected status code: " + response.getCode()); throw new IllegalStateException("Unexpected status code: " + response.getCode());
} }
if (linkType.isAssignableFrom(response.getClass())) { if (linkType.isAssignableFrom(ClassicHttpResponse.class)) {
return (T)response; delegatedRelease.set(true);
return (T)Proxy.newProxyInstance(
ClassicHttpResponse.class.getClassLoader(), new Class<?>[] { ClassicHttpResponse.class },
(proxy, method, args) -> {
if (ObjectUtils.isEmpty(method.getParameterTypes()) && method.getName().equals("close")) {
response.close();
key.release();
return null;
} else {
try {
return method.invoke(response, args);
} catch (final InvocationTargetException e) {
throw e.getCause() == null ? e : e.getCause();
}
}
});
} else if (linkType == InputStream.class) { } else if (linkType == InputStream.class) {
return (T)response.getEntity().getContent(); final InputStream is = response.getEntity().getContent();
delegatedRelease.set(true);
return (T)new InputStream() {
@Override
public int read() throws IOException {
return is.read();
}
@Override
public int read(final byte[] b, final int off, final int read) throws IOException {
return is.read(b, off, read);
}
@Override
public void close() throws IOException {
try {
is.close();
} finally {
key.release();
}
}
};
} else { } else {
return new ObjectMapper().readValue(response.getEntity().getContent(), linkType); return new ObjectMapper().readValue(response.getEntity().getContent(), linkType);
} }
}); });
} finally { } finally {
synchronized (HTTP_CLIENTS) { if (!delegatedRelease.get()) {
HTTP_CLIENTS.get(key).release(); key.release();
} }
} }
} }
@@ -211,11 +257,7 @@ public class HawkbitClient {
.contract(contract) .contract(contract)
.requestInterceptor(requestInterceptorFn.apply(tenant, controller)) .requestInterceptor(requestInterceptorFn.apply(tenant, controller))
.target(serviceType, url); .target(serviceType, url);
CLEANER.register(service, () -> { CLEANER.register(service, key::release);
synchronized (HTTP_CLIENTS) {
HTTP_CLIENTS.get(key).release();
}
});
return service; return service;
} }
@@ -446,6 +488,12 @@ public class HawkbitClient {
private final boolean https; private final boolean https;
private final Certificate clientCertificate; private final Certificate clientCertificate;
private final X509Certificate[] serverCertificates; private final X509Certificate[] serverCertificates;
private void release() {
synchronized (HTTP_CLIENTS) {
HTTP_CLIENTS.get(this).release();
}
}
} }
private static class HttpClientWrapper { private static class HttpClientWrapper {