From 4d0c8d351c6df13c98cc9e9ce5384f183aa7e799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20St=C3=B8a?= Date: Tue, 7 Feb 2023 17:04:18 +0100 Subject: [PATCH 01/32] Removes spring/jersey and up min supported java version to 11 --- .github/workflows/build.yml | 2 +- .java-version | 2 +- pom.xml | 63 +--- .../signature/client/Certificates.java | 2 +- .../signature/client/ClientConfiguration.java | 166 ++++----- .../client/archive/ArchiveClient.java | 3 +- .../asice/manifest/ManifestCreator.java | 10 - .../asice/signature/CreateSignature.java | 78 +++- .../asice/signature/XAdESArtifacts.java | 23 +- .../exceptions/CantQueryStatusException.java | 2 +- .../JobCannotBeCancelledException.java | 2 +- .../UnexpectedResponseException.java | 4 +- .../core/internal/ClientExceptionMapper.java | 2 +- .../client/core/internal/ClientHelper.java | 334 +++++++++++------- .../internal/http/AddRequestHeaderFilter.java | 8 +- .../http/HttpIntegrationConfiguration.java | 13 +- .../core/internal/http/ResponseStatus.java | 6 +- .../internal/http/SignatureHttpClient.java | 10 +- .../http/SignatureHttpClientFactory.java | 66 +--- .../client/core/internal/http/UriHelper.java | 22 ++ .../xml/JaxbMessageReaderWriterProvider.java | 44 --- .../client/core/internal/xml/Marshalling.java | 10 +- .../signature/client/direct/DirectClient.java | 4 +- .../client/direct/StatusReference.java | 4 +- .../signature/client/portal/PortalClient.java | 4 +- .../asice/signature/CreateSignatureTest.java | 34 +- .../internal/http/ResponseStatusTest.java | 2 +- .../http/SignatureHttpClientFactoryTest.java | 30 +- .../core/internal/http/UriHelperTest.java | 25 ++ 29 files changed, 495 insertions(+), 480 deletions(-) create mode 100644 src/main/java/no/digipost/signature/client/core/internal/http/UriHelper.java delete mode 100644 src/main/java/no/digipost/signature/client/core/internal/xml/JaxbMessageReaderWriterProvider.java create mode 100644 src/test/java/no/digipost/signature/client/core/internal/http/UriHelperTest.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6fe4e73..7b9b2126 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '1.8', '11' ] + java: [ '11' ] name: build java ${{ matrix.java }} steps: diff --git a/.java-version b/.java-version index 62593409..b4de3947 100644 --- a/.java-version +++ b/.java-version @@ -1 +1 @@ -1.8 +11 diff --git a/pom.xml b/pom.xml index f4dbebd7..a35609dc 100644 --- a/pom.xml +++ b/pom.xml @@ -14,29 +14,15 @@ - 1.8 - 1.8 + 11 + 11 set_with_-Dproject.previousVersion=X.Y - 2.8 + LOCAL-SNAPSHOT 1.7.36 - - org.springframework - spring-framework-bom - 5.3.21 - pom - import - - - org.glassfish.jersey - jersey-bom - 2.36 - pom - import - org.junit junit-bom @@ -89,34 +75,22 @@ jakarta.annotation jakarta.annotation-api - 1.3.5 + 2.1.1 jakarta.ws.rs jakarta.ws.rs-api - 2.1.6 - - - org.glassfish.jaxb - jaxb-runtime - 2.3.4 - runtime + 3.1.0 - org.glassfish.jersey.core - jersey-client + jakarta.xml.bind + jakarta.xml.bind-api + 2.3.3 - org.glassfish.jersey.core - jersey-common - - - org.glassfish.jersey.media - jersey-media-multipart - - - org.glassfish.jersey.inject - jersey-hk2 + org.glassfish.jaxb + jaxb-runtime + 2.3.5 runtime @@ -130,19 +104,6 @@ commons-codec 1.15 - - org.springframework - spring-oxm - - - org.springframework - spring-core - - - org.springframework.ws - spring-xml - 3.1.3 - org.apache.commons commons-lang3 @@ -238,7 +199,7 @@ org.glassfish.jaxb jaxb-runtime - 2.3.4 + 2.3.6 jakarta.xml.bind diff --git a/src/main/java/no/digipost/signature/client/Certificates.java b/src/main/java/no/digipost/signature/client/Certificates.java index 9dc89879..941895ff 100644 --- a/src/main/java/no/digipost/signature/client/Certificates.java +++ b/src/main/java/no/digipost/signature/client/Certificates.java @@ -35,7 +35,7 @@ public enum Certificates { "prod/commfides_root_ca.cer" ); - final List certificatePaths; + public final List certificatePaths; Certificates(String ... certificatePaths) { this.certificatePaths = Stream.of(certificatePaths) diff --git a/src/main/java/no/digipost/signature/client/ClientConfiguration.java b/src/main/java/no/digipost/signature/client/ClientConfiguration.java index 796b59f6..684a66b8 100644 --- a/src/main/java/no/digipost/signature/client/ClientConfiguration.java +++ b/src/main/java/no/digipost/signature/client/ClientConfiguration.java @@ -1,42 +1,36 @@ package no.digipost.signature.client; +import jakarta.ws.rs.core.HttpHeaders; import no.digipost.signature.client.asice.ASiCEConfiguration; import no.digipost.signature.client.asice.DocumentBundleProcessor; import no.digipost.signature.client.asice.DumpDocumentBundleToDisk; import no.digipost.signature.client.core.Sender; import no.digipost.signature.client.core.SignatureJob; import no.digipost.signature.client.core.exceptions.KeyException; -import no.digipost.signature.client.core.internal.http.AddRequestHeaderFilter; import no.digipost.signature.client.core.internal.http.HttpIntegrationConfiguration; import no.digipost.signature.client.core.internal.http.SignatureApiTrustStrategy; import no.digipost.signature.client.core.internal.security.ProvidesCertificateResourcePaths; import no.digipost.signature.client.core.internal.security.TrustStoreLoader; -import no.digipost.signature.client.core.internal.xml.JaxbMessageReaderWriterProvider; import no.digipost.signature.client.security.CertificateChainValidation; import no.digipost.signature.client.security.KeyStoreConfig; import no.digipost.signature.client.security.OrganizationNumberValidation; import org.apache.commons.lang3.StringUtils; import org.apache.http.ssl.SSLContexts; -import org.glassfish.jersey.client.ClientConfig; -import org.glassfish.jersey.client.ClientProperties; -import org.glassfish.jersey.logging.LoggingFeature; -import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.SSLContext; -import javax.ws.rs.core.Configurable; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.HttpHeaders; - import java.io.InputStream; +import java.net.ProxySelector; import java.net.URI; +import java.net.http.HttpClient; import java.nio.file.Path; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.time.Clock; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -44,7 +38,6 @@ import java.util.function.Consumer; import static java.util.Arrays.asList; -import static javax.ws.rs.core.HttpHeaders.USER_AGENT; import static no.digipost.signature.client.Certificates.TEST; import static no.digipost.signature.client.ClientMetadata.VERSION; @@ -71,39 +64,38 @@ public final class ClientConfiguration implements ProvidesCertificateResourcePat /** * Socket timeout is used for both requests and, if any, * underlying layered sockets (typically for - * secure sockets). The default value is {@value #DEFAULT_SOCKET_TIMEOUT_MS} ms. + * secure sockets). */ - public static final int DEFAULT_SOCKET_TIMEOUT_MS = 10_000; + public static final Duration DEFAULT_SOCKET_TIMEOUT = Duration.ofSeconds(10); /** - * The default connect timeout for requests: {@value #DEFAULT_CONNECT_TIMEOUT_MS} ms. + * The default connect timeout for requests. */ - public static final int DEFAULT_CONNECT_TIMEOUT_MS = 10_000; + public static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10); - private final Configurable jaxrsConfig; private final KeyStoreConfig keyStoreConfig; - private final Iterable certificatePaths; + private final HttpClient httpClient; + private final Duration socketTimeout; private final Optional sender; private final URI signatureServiceRoot; + private final Iterable certificatePaths; private final Iterable documentBundleProcessors; - private final CertificateChainValidation serverCertificateValidation; private final Clock clock; private ClientConfiguration( - KeyStoreConfig keyStoreConfig, Configurable jaxrsConfig, - Optional sender, URI serviceRoot, Iterable certificatePaths, - Iterable documentBundleProcessors, CertificateChainValidation serverCertificateValidation, Clock clock) { + KeyStoreConfig keyStoreConfig, HttpClient httpClient, Duration socketTimeout, Optional sender, + URI serviceRoot, Iterable certificatePaths, Iterable documentBundleProcessors, Clock clock) { - this.jaxrsConfig = jaxrsConfig; this.keyStoreConfig = keyStoreConfig; - this.certificatePaths = certificatePaths; + this.httpClient = httpClient; + this.socketTimeout = socketTimeout; this.sender = sender; this.signatureServiceRoot = serviceRoot; + this.certificatePaths = certificatePaths; this.documentBundleProcessors = documentBundleProcessors; - this.serverCertificateValidation = serverCertificateValidation; this.clock = clock; } @@ -134,43 +126,15 @@ public URI getServiceRoot() { } @Override - public Iterable getCertificatePaths() { - return certificatePaths; + public HttpClient httpClient() { + return httpClient; } - - /** - * Get the JAX-RS {@link Configuration} based on the current state of this {@link ClientConfiguration}. - * - * @return the JAX-RS {@link Configuration} - */ @Override - public Configuration getJaxrsConfiguration() { - return jaxrsConfig.getConfiguration(); + public Duration socketTimeout() { + return socketTimeout; } - - @Override - public SSLContext getSSLContext() { - try { - return SSLContexts.custom() - .loadKeyMaterial(keyStoreConfig.keyStore, keyStoreConfig.privatekeyPassword.toCharArray(), (aliases, socket) -> keyStoreConfig.alias) - .loadTrustMaterial(TrustStoreLoader.build(this), new SignatureApiTrustStrategy(serverCertificateValidation)) - .build(); - } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | UnrecoverableKeyException e) { - if (e instanceof UnrecoverableKeyException && "Given final block not properly padded".equals(e.getMessage())) { - throw new KeyException( - "Unable to load key from keystore, because " + e.getClass().getSimpleName() + ": '" + e.getMessage() + "'. Possible causes:\n" + - "* Wrong password for private key (the password for the keystore and the private key may not be the same)\n" + - "* Multiple private keys in the keystore with different passwords (private keys in the same key store must have the same password)", e); - } else { - throw new KeyException("Unable to create the SSLContext, because " + e.getClass().getSimpleName() + ": '" + e.getMessage() + "'", e); - } - } - } - - - /** * Build a new {@link ClientConfiguration}. */ @@ -178,26 +142,32 @@ public static Builder builder(KeyStoreConfig keystore) { return new Builder(keystore); } + public Iterable getCertificatePaths() { + return certificatePaths; + } + + public static class Builder { - private final Configurable jaxrsConfig; + private final HttpClient.Builder httpClientBuilder; private final KeyStoreConfig keyStoreConfig; - private int socketTimeoutMs = DEFAULT_SOCKET_TIMEOUT_MS; - private int connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS; + private Duration socketTimeout = DEFAULT_SOCKET_TIMEOUT; private Optional customUserAgentPart = Optional.empty(); private URI serviceRoot = ServiceUri.PRODUCTION.uri; private Optional globalSender = Optional.empty(); private Iterable certificatePaths = Certificates.PRODUCTION.certificatePaths; private CertificateChainValidation serverCertificateTrustStrategy = new OrganizationNumberValidation("984661185"); // Posten Norge AS organization number - private Optional loggingFeature = Optional.empty(); private List documentBundleProcessors = new ArrayList<>(); private Clock clock = Clock.systemDefaultZone(); + private Optional proxy = Optional.empty(); private Builder(KeyStoreConfig keyStoreConfig) { this.keyStoreConfig = keyStoreConfig; - this.jaxrsConfig = new ClientConfig(); + this.httpClientBuilder = HttpClient.newBuilder() + .connectTimeout(DEFAULT_CONNECT_TIMEOUT) + .followRedirects(HttpClient.Redirect.ALWAYS); } /** @@ -217,22 +187,29 @@ public Builder serviceUri(URI uri) { /** * Override the - * {@link ClientConfiguration#DEFAULT_SOCKET_TIMEOUT_MS default socket timeout value}. + * {@link ClientConfiguration#DEFAULT_SOCKET_TIMEOUT default socket timeout value}. */ - public Builder socketTimeoutMillis(int millis) { - this.socketTimeoutMs = millis; + public Builder socketTimeout(Duration timeout) { + this.socketTimeout = timeout; return this; } /** * Override the - * {@link ClientConfiguration#DEFAULT_CONNECT_TIMEOUT_MS default connect timeout value}. + * {@link ClientConfiguration#DEFAULT_CONNECT_TIMEOUT default connect timeout value}. */ - public Builder connectTimeoutMillis(int millis) { - this.connectTimeoutMs = millis; + public Builder connectTimeout(Duration timeout) { + this.httpClientBuilder.connectTimeout(timeout); return this; } + /** + * Set proxy to be used by {@link HttpClient}. + */ + public void proxy(ProxySelector proxy) { + this.proxy = Optional.of(proxy); + } + public Builder trustStore(Certificates certificates) { if (certificates == TEST) { LOG.warn("Using test certificates in trust store. This should never be done for production environments."); @@ -240,7 +217,6 @@ public Builder trustStore(Certificates certificates) { return trustStore(certificates.certificatePaths); } - /** * Override the trust store configuration to load DER-encoded certificates from the given folder(s). * @@ -288,7 +264,7 @@ public Builder includeInUserAgent(String userAgentCustomPart) { * {@link ClientConfiguration#HTTP_REQUEST_RESPONSE_LOGGER_NAME}. */ public Builder enableRequestAndResponseLogging() { - loggingFeature = Optional.of(new LoggingFeature(java.util.logging.Logger.getLogger(HTTP_REQUEST_RESPONSE_LOGGER_NAME), 16 * 1024)); + //loggingFeature = Optional.of(new LoggingFeature(java.util.logging.Logger.getLogger(HTTP_REQUEST_RESPONSE_LOGGER_NAME), 16 * 1024)); return this; } @@ -330,7 +306,7 @@ public Builder addDocumentBundleProcessor(DocumentBundleProcessor processor) { } /** - * This methods allows for custom configuration of JAX-RS (i.e. Jersey) if anything is + * This methods allows for custom configuration of the {@link HttpClient} if anything is * needed that is not already supported by the {@link ClientConfiguration.Builder}. * This method should not be used to configure anything that is already directly supported by the * {@code ClientConfiguration.Builder} API. @@ -338,11 +314,10 @@ public Builder addDocumentBundleProcessor(DocumentBundleProcessor processor) { * If you still need to use this method, consider requesting first-class support for your requirement * on the library's web site on GitHub. * - * @param customizer The operations to do on the JAX-RS {@link Configurable}, e.g. - * {@link Configurable#register(Object) registering components}. + * @param customizer The operations to do on the {@link HttpClient.Builder}. */ - public Builder customizeJaxRs(Consumer> customizer) { - customizer.accept(jaxrsConfig); + public Builder customizeHttpClient(Consumer customizer) { + customizer.accept(this.httpClientBuilder); return this; } @@ -386,34 +361,35 @@ public Builder usingClock(Clock clock) { return this; } - - /** - * Disable the pre-initialization step of the internal HTTP client (Jersey Client) when - * instantiating the Signature API Client. - * - * @see org.glassfish.jersey.client.JerseyClient#preInitialize() - */ - public Builder disablePreInitializingHttpClient() { - this.jaxrsConfig.property(PRE_INIT_CLIENT, false); - return this; - } - public ClientConfiguration build() { - jaxrsConfig.property(ClientProperties.READ_TIMEOUT, socketTimeoutMs); - jaxrsConfig.property(ClientProperties.CONNECT_TIMEOUT, connectTimeoutMs); - jaxrsConfig.register(MultiPartFeature.class); - jaxrsConfig.register(JaxbMessageReaderWriterProvider.class); - jaxrsConfig.register(new AddRequestHeaderFilter(USER_AGENT, createUserAgentString())); - this.loggingFeature.ifPresent(jaxrsConfig::register); - return new ClientConfiguration( - keyStoreConfig, jaxrsConfig, globalSender, serviceRoot, certificatePaths, - documentBundleProcessors, serverCertificateTrustStrategy, clock); + // TODO: Add possibility to add logger for http client + var httpClient = httpClientBuilder + .sslContext(sslContext()); + proxy.ifPresent(httpClient::proxy); + return new ClientConfiguration(keyStoreConfig, httpClient.build(), socketTimeout, globalSender, serviceRoot, certificatePaths, documentBundleProcessors, clock); } String createUserAgentString() { return MANDATORY_USER_AGENT + customUserAgentPart.map(ua -> String.format(" (%s)", ua)).orElse(""); } + private SSLContext sslContext() { + try { + return SSLContexts.custom() + .loadKeyMaterial(keyStoreConfig.keyStore, keyStoreConfig.privatekeyPassword.toCharArray(), (aliases, socket) -> keyStoreConfig.alias) + .loadTrustMaterial(TrustStoreLoader.build(() -> certificatePaths), new SignatureApiTrustStrategy(serverCertificateTrustStrategy)) + .build(); + } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | UnrecoverableKeyException e) { + if (e instanceof UnrecoverableKeyException && "Given final block not properly padded".equals(e.getMessage())) { + throw new KeyException( + "Unable to load key from keystore, because " + e.getClass().getSimpleName() + ": '" + e.getMessage() + "'. Possible causes:\n" + + "* Wrong password for private key (the password for the keystore and the private key may not be the same)\n" + + "* Multiple private keys in the keystore with different passwords (private keys in the same key store must have the same password)", e); + } else { + throw new KeyException("Unable to create the SSLContext, because " + e.getClass().getSimpleName() + ": '" + e.getMessage() + "'", e); + } + } + } } } diff --git a/src/main/java/no/digipost/signature/client/archive/ArchiveClient.java b/src/main/java/no/digipost/signature/client/archive/ArchiveClient.java index b3075bbe..ec8c7e89 100644 --- a/src/main/java/no/digipost/signature/client/archive/ArchiveClient.java +++ b/src/main/java/no/digipost/signature/client/archive/ArchiveClient.java @@ -5,6 +5,7 @@ import no.digipost.signature.client.core.internal.http.HttpIntegrationConfiguration; import no.digipost.signature.client.core.internal.http.SignatureHttpClientFactory; +import java.net.URI; import java.util.Optional; public class ArchiveClient { @@ -16,7 +17,7 @@ public ArchiveClient(HttpIntegrationConfiguration httpIntegrationConfig) { } public ResponseInputStream getPAdES(ArchiveOwner owner, String id) { - return client.getDataStream(root -> root.path(owner.getOrganizationNumber()).path("archive/documents/").path(id).path("pades")); + return client.getDataStream(URI.create(owner.getOrganizationNumber() + "/archive/documents/" + id + "/pades")); } } diff --git a/src/main/java/no/digipost/signature/client/asice/manifest/ManifestCreator.java b/src/main/java/no/digipost/signature/client/asice/manifest/ManifestCreator.java index 230f32fd..05ba8aed 100644 --- a/src/main/java/no/digipost/signature/client/asice/manifest/ManifestCreator.java +++ b/src/main/java/no/digipost/signature/client/asice/manifest/ManifestCreator.java @@ -3,9 +3,6 @@ import no.digipost.signature.client.core.Sender; import no.digipost.signature.client.core.SignatureJob; import no.digipost.signature.client.core.exceptions.RuntimeIOException; -import no.digipost.signature.client.core.exceptions.XmlValidationException; -import org.springframework.oxm.MarshallingFailureException; -import org.xml.sax.SAXParseException; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -20,13 +17,6 @@ public Manifest createManifest(JOB job, Sender sender) { try (ByteArrayOutputStream manifestStream = new ByteArrayOutputStream()) { marshal(xmlManifest, manifestStream); return new Manifest(manifestStream.toByteArray()); - } catch (MarshallingFailureException e) { - if (e.getMostSpecificCause() instanceof SAXParseException) { - throw new XmlValidationException("Unable to validate generated Manifest XML. " + - "This typically happens if one or more values are not in accordance with the XSD. " + - "You may inspect the cause (by calling getCause()) to see which constraint has been violated.", (SAXParseException) e.getMostSpecificCause()); - } - throw e; } catch (IOException e) { throw new RuntimeIOException(e); } diff --git a/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java b/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java index f20c4fc9..7c4cbc4e 100644 --- a/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java +++ b/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java @@ -4,50 +4,53 @@ import no.digipost.signature.client.core.exceptions.XmlConfigurationException; import no.digipost.signature.client.core.exceptions.XmlValidationException; import no.digipost.signature.client.security.KeyStoreConfig; +import no.digipost.signature.jaxb.JaxbMarshaller; import no.digipost.signature.xsd.SignatureApiSchemas; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; -import org.springframework.xml.validation.SchemaLoaderUtils; -import org.springframework.xml.validation.XmlValidatorFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.xml.sax.InputSource; import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import javax.xml.XMLConstants; import javax.xml.crypto.MarshalException; import javax.xml.crypto.NodeSetData; import javax.xml.crypto.URIDereferencer; import javax.xml.crypto.dom.DOMStructure; -import javax.xml.crypto.dsig.CanonicalizationMethod; -import javax.xml.crypto.dsig.DigestMethod; -import javax.xml.crypto.dsig.Reference; -import javax.xml.crypto.dsig.SignatureMethod; -import javax.xml.crypto.dsig.SignedInfo; -import javax.xml.crypto.dsig.Transform; -import javax.xml.crypto.dsig.XMLObject; -import javax.xml.crypto.dsig.XMLSignature; -import javax.xml.crypto.dsig.XMLSignatureException; -import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.*; import javax.xml.crypto.dsig.dom.DOMSignContext; import javax.xml.crypto.dsig.keyinfo.KeyInfo; import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; import javax.xml.crypto.dsig.keyinfo.X509Data; import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; import javax.xml.crypto.dsig.spec.TransformParameterSpec; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.Source; import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXSource; import javax.xml.validation.Schema; - +import javax.xml.validation.SchemaFactory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; import java.io.UnsupportedEncodingException; +import java.net.URL; import java.net.URLEncoder; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.time.Clock; import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Set; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; public class CreateSignature { @@ -82,14 +85,51 @@ public CreateSignature(Clock clock) { schema = loadSchema(); } - private Schema loadSchema() { + private static InputSource createInputSource(String resource) { + URL resourceUrl = requireNonNull(JaxbMarshaller.class.getResource(resource), resource); + try (InputStream inputStream = resourceUrl.openStream()) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + final int bufLen = 1024; + byte[] buf = new byte[bufLen]; + int readLen; + while ((readLen = inputStream.read(buf, 0, bufLen)) != -1) + outputStream.write(buf, 0, readLen); + + InputSource source = new InputSource(new ByteArrayInputStream(outputStream.toByteArray())); + source.setSystemId(resourceUrl.toString()); + return source; + } catch (IOException e) { + throw new UncheckedIOException( + "Unable to resolve " + resource + " from " + resourceUrl + ", " + + "because " + e.getClass().getSimpleName() + " '" + e.getMessage() + "'", e); + } + } + + private static Schema createSchema(Collection resources) { try { - return SchemaLoaderUtils.loadSchema(new Resource[]{new ClassPathResource(SignatureApiSchemas.XMLDSIG_SCHEMA), new ClassPathResource(SignatureApiSchemas.ASICE_SCHEMA)}, XmlValidatorFactory.SCHEMA_W3C_XML); - } catch (IOException | SAXException e) { - throw new ConfigurationException("Failed to load schemas for validating signatures", e); + SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + parserFactory.setNamespaceAware(true); + parserFactory.setFeature("http://xml.org/sax/features/namespace-prefixes", true); + + SAXParser saxParser = parserFactory.newSAXParser(); + XMLReader xmlReader = saxParser.getXMLReader(); + Source[] schemaSources = resources.stream() + .map(resource -> new SAXSource(xmlReader, createInputSource(resource))) + .toArray(Source[]::new); + + SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + Schema schema = schemaFactory.newSchema(schemaSources); + return schema; + } catch (Exception e) { + throw new RuntimeException("Could not create schema from resources [" + String.join(", ", resources) + "]", e); } } + private Schema loadSchema() { + return createSchema(Set.of(SignatureApiSchemas.XMLDSIG_SCHEMA, SignatureApiSchemas.ASICE_SCHEMA)); + } + public Signature createSignature(final List attachedFiles, final KeyStoreConfig keyStoreConfig) { return new Signature(domUtils.serializeToXml(createXmlSignature(attachedFiles, keyStoreConfig))); } diff --git a/src/main/java/no/digipost/signature/client/asice/signature/XAdESArtifacts.java b/src/main/java/no/digipost/signature/client/asice/signature/XAdESArtifacts.java index 54cfb94a..c640c4c4 100644 --- a/src/main/java/no/digipost/signature/client/asice/signature/XAdESArtifacts.java +++ b/src/main/java/no/digipost/signature/client/asice/signature/XAdESArtifacts.java @@ -1,30 +1,39 @@ package no.digipost.signature.client.asice.signature; import no.digipost.signature.api.xml.thirdparty.xades.QualifyingProperties; -import org.springframework.oxm.jaxb.Jaxb2Marshaller; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; import javax.xml.transform.dom.DOMResult; import static java.util.stream.IntStream.range; final class XAdESArtifacts { - private static Jaxb2Marshaller marshaller; + private static Marshaller marshaller; static { - marshaller = new Jaxb2Marshaller(); - marshaller.setClassesToBeBound(QualifyingProperties.class); + try { + marshaller = JAXBContext.newInstance(QualifyingProperties.class).createMarshaller(); + } catch (JAXBException e) { + throw new RuntimeException(e); + } } public static XAdESArtifacts from(QualifyingProperties qualifyingProperties) { - DOMResult domResult = new DOMResult(); - marshaller.marshal(qualifyingProperties, domResult); - return from((Document) domResult.getNode()); + try { + DOMResult domResult = new DOMResult(); + marshaller.marshal(qualifyingProperties, domResult); + return from((Document) domResult.getNode()); + } catch (JAXBException e) { + throw new RuntimeException(e); + } } private static XAdESArtifacts from(Document qualifyingPropertiesDocument) { diff --git a/src/main/java/no/digipost/signature/client/core/exceptions/CantQueryStatusException.java b/src/main/java/no/digipost/signature/client/core/exceptions/CantQueryStatusException.java index c0e4bce5..e0fb8de0 100644 --- a/src/main/java/no/digipost/signature/client/core/exceptions/CantQueryStatusException.java +++ b/src/main/java/no/digipost/signature/client/core/exceptions/CantQueryStatusException.java @@ -1,6 +1,6 @@ package no.digipost.signature.client.core.exceptions; -import javax.ws.rs.core.Response.StatusType; +import jakarta.ws.rs.core.Response.StatusType; public class CantQueryStatusException extends SignatureException { diff --git a/src/main/java/no/digipost/signature/client/core/exceptions/JobCannotBeCancelledException.java b/src/main/java/no/digipost/signature/client/core/exceptions/JobCannotBeCancelledException.java index 13f24acd..64e55c4f 100644 --- a/src/main/java/no/digipost/signature/client/core/exceptions/JobCannotBeCancelledException.java +++ b/src/main/java/no/digipost/signature/client/core/exceptions/JobCannotBeCancelledException.java @@ -2,7 +2,7 @@ import no.digipost.signature.api.xml.XMLError; -import javax.ws.rs.core.Response.StatusType; +import jakarta.ws.rs.core.Response.StatusType; public class JobCannotBeCancelledException extends SignatureException { diff --git a/src/main/java/no/digipost/signature/client/core/exceptions/UnexpectedResponseException.java b/src/main/java/no/digipost/signature/client/core/exceptions/UnexpectedResponseException.java index b8456ead..bfe91c09 100644 --- a/src/main/java/no/digipost/signature/client/core/exceptions/UnexpectedResponseException.java +++ b/src/main/java/no/digipost/signature/client/core/exceptions/UnexpectedResponseException.java @@ -2,8 +2,8 @@ import no.digipost.signature.api.xml.XMLError; -import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.Response.StatusType; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.Response.StatusType; import java.util.Objects; diff --git a/src/main/java/no/digipost/signature/client/core/internal/ClientExceptionMapper.java b/src/main/java/no/digipost/signature/client/core/internal/ClientExceptionMapper.java index 62c221a2..86bd5054 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/ClientExceptionMapper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/ClientExceptionMapper.java @@ -5,7 +5,7 @@ import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; -import javax.ws.rs.ProcessingException; +import jakarta.ws.rs.ProcessingException; import java.util.function.Supplier; class ClientExceptionMapper { diff --git a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java index ead50461..86133e26 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java @@ -20,50 +20,51 @@ import no.digipost.signature.client.core.exceptions.InvalidStatusQueryTokenException; import no.digipost.signature.client.core.exceptions.JobCannotBeCancelledException; import no.digipost.signature.client.core.exceptions.NotCancellableException; -import no.digipost.signature.client.core.exceptions.RuntimeIOException; import no.digipost.signature.client.core.exceptions.SignatureException; import no.digipost.signature.client.core.exceptions.TooEagerPollingException; import no.digipost.signature.client.core.exceptions.UnexpectedResponseException; import no.digipost.signature.client.core.internal.http.ResponseStatus; import no.digipost.signature.client.core.internal.http.SignatureHttpClient; +import no.digipost.signature.client.core.internal.http.UriBuilder; +import no.digipost.signature.client.core.internal.xml.Marshalling; import no.digipost.signature.client.direct.WithSignerUrl; import org.apache.commons.lang3.StringUtils; -import org.glassfish.jersey.media.multipart.BodyPart; -import org.glassfish.jersey.media.multipart.MultiPart; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.Invocation; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.StatusType; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response.StatusType; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; +import java.math.BigInteger; import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.ZonedDateTime; +import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.Optional; +import java.util.Random; import java.util.function.Supplier; -import java.util.function.UnaryOperator; +import static jakarta.ws.rs.core.HttpHeaders.*; import static java.time.format.DateTimeFormatter.ISO_DATE_TIME; -import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH; -import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; -import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE; -import static javax.ws.rs.core.MediaType.APPLICATION_XML_TYPE; -import static javax.ws.rs.core.Response.Status.CONFLICT; -import static javax.ws.rs.core.Response.Status.FORBIDDEN; -import static javax.ws.rs.core.Response.Status.NOT_FOUND; -import static javax.ws.rs.core.Response.Status.NO_CONTENT; -import static javax.ws.rs.core.Response.Status.OK; -import static javax.ws.rs.core.Response.Status.TOO_MANY_REQUESTS; -import static javax.ws.rs.core.Response.Status.Family.SUCCESSFUL; +import static jakarta.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE; +import static jakarta.ws.rs.core.MediaType.APPLICATION_XML_TYPE; +import static jakarta.ws.rs.core.Response.Status.CONFLICT; +import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; +import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; +import static jakarta.ws.rs.core.Response.Status.NO_CONTENT; +import static jakarta.ws.rs.core.Response.Status.OK; +import static jakarta.ws.rs.core.Response.Status.TOO_MANY_REQUESTS; +import static jakarta.ws.rs.core.Response.Status.Family.SUCCESSFUL; import static no.digipost.signature.client.core.internal.ActualSender.getActualSender; import static no.digipost.signature.client.core.internal.ErrorCodes.BROKER_NOT_AUTHORIZED; import static no.digipost.signature.client.core.internal.ErrorCodes.SIGNING_CEREMONY_NOT_COMPLETED; @@ -90,75 +91,137 @@ public ClientHelper(SignatureHttpClient httpClient, Optional globalSende public XMLDirectSignatureJobResponse sendSignatureJobRequest(XMLDirectSignatureJobRequest signatureJobRequest, DocumentBundle documentBundle, Optional sender) { final Sender actualSender = getActualSender(sender, globalSender); - final BodyPart signatureJobBodyPart = new BodyPart(signatureJobRequest, APPLICATION_XML_TYPE); - final BodyPart documentBundleBodyPart = new BodyPart(documentBundle.getInputStream(), APPLICATION_OCTET_STREAM_TYPE); - - return call(() -> new UsingBodyParts(signatureJobBodyPart, documentBundleBodyPart) - .postAsMultiPart(DIRECT.path(actualSender), XMLDirectSignatureJobResponse.class)); - } - - public XMLDirectSignerResponse requestNewRedirectUrl(WithSignerUrl url) { - try (Response response = postEntity(url.getSignerUrl(), new XMLDirectSignerUpdateRequest().withRedirectUrl(new XMLEmptyElement()))) { - return parseResponse(response, XMLDirectSignerResponse.class); - } + return multipartSignatureJobRequest(signatureJobRequest, documentBundle, actualSender, XMLDirectSignatureJobResponse.class); } public XMLPortalSignatureJobResponse sendPortalSignatureJobRequest(XMLPortalSignatureJobRequest signatureJobRequest, DocumentBundle documentBundle, Optional sender) { final Sender actualSender = getActualSender(sender, globalSender); - final BodyPart signatureJobBodyPart = new BodyPart(signatureJobRequest, APPLICATION_XML_TYPE); - final BodyPart documentBundleBodyPart = new BodyPart(documentBundle.getInputStream(), APPLICATION_OCTET_STREAM_TYPE); + return multipartSignatureJobRequest(signatureJobRequest, documentBundle, actualSender, XMLPortalSignatureJobResponse.class); + } + + private RESPONSE multipartSignatureJobRequest(REQUEST signatureJobRequest, DocumentBundle documentBundle, Sender actualSender, Class responseClass) { + return call(() -> { + try { + String boundary = new BigInteger(256, new Random()).toString(); + var byteArrays = new ArrayList(); + byte[] separator = ("--" + boundary + "\r\nContent-Disposition: form-data; name=") + .getBytes(StandardCharsets.UTF_8); + + byteArrays.add(separator); + + try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { + Marshalling.marshal(signatureJobRequest, os); + byteArrays.add(("\"\r\nContent-Type: " + APPLICATION_XML_TYPE.getType() + "\r\n\r\n").getBytes(StandardCharsets.UTF_8)); + byteArrays.add(os.toByteArray()); + } + + byteArrays.add(separator); + + byteArrays.add(("\"\r\nContent-Type: " + APPLICATION_OCTET_STREAM_TYPE.getType() + "\r\n\r\n").getBytes(StandardCharsets.UTF_8)); + byteArrays.add(documentBundle.getInputStream().readAllBytes()); + + byteArrays.add(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8)); - return call(() -> new UsingBodyParts(signatureJobBodyPart, documentBundleBodyPart) - .postAsMultiPart(PORTAL.path(actualSender), XMLPortalSignatureJobResponse.class)); + var request = HttpRequest.newBuilder() + .uri(httpClient.signatureServiceRoot().resolve(DIRECT.path(actualSender))) + .header("Content-Type", "multipart/form-data;boundary=" + boundary) + .header(ACCEPT, APPLICATION_XML_TYPE.getType()) + .POST(HttpRequest.BodyPublishers.ofByteArrays(byteArrays)) + .build(); + + var response = httpClient.httpClient().send(request, BodyHandlers.ofInputStream()); + return parseResponse(response, responseClass); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + } + + public XMLDirectSignerResponse requestNewRedirectUrl(WithSignerUrl url) { + // TODO: Er det noe annet å bruke i stede for? + try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { + Marshalling.marshal(new XMLDirectSignerUpdateRequest().withRedirectUrl(new XMLEmptyElement()), os); + var request = HttpRequest.newBuilder() + .uri(url.getSignerUrl()) + .header(ACCEPT, APPLICATION_XML_TYPE.getType()) + .POST(HttpRequest.BodyPublishers.ofByteArray(os.toByteArray())) + .build(); + + var result = httpClient.httpClient().send(request, BodyHandlers.ofInputStream()); + return parseResponse(result, XMLDirectSignerResponse.class); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } } public XMLDirectSignatureJobStatusResponse sendSignatureJobStatusRequest(final URI statusUrl) { return call(() -> { - Invocation.Builder request = httpClient.target(statusUrl).request().accept(APPLICATION_XML_TYPE); - - try (Response response = request.get()) { - ResponseStatus.resolve(response.getStatus()).expect(SUCCESSFUL).orThrow(status -> { + var request = HttpRequest.newBuilder() + .uri(httpClient.signatureServiceRoot().resolve(statusUrl)) + .header(ACCEPT, APPLICATION_XML_TYPE.getType()) + .GET() + .build(); + try { + var result = httpClient.httpClient().send(request, BodyHandlers.ofInputStream()); + ResponseStatus.resolve(result.statusCode()).expect(SUCCESSFUL).orThrow(status -> { if (status == FORBIDDEN) { - XMLError error = extractError(response); + XMLError error = extractError(result); if (ErrorCodes.INVALID_STATUS_QUERY_TOKEN.sameAs(error.getErrorCode())) { return new InvalidStatusQueryTokenException(statusUrl, error.getErrorMessage()); } } else if (status == NOT_FOUND) { - XMLError error = extractError(response); + XMLError error = extractError(result); if (SIGNING_CEREMONY_NOT_COMPLETED.sameAs(error.getErrorCode())) { return new CantQueryStatusException(status, error.getErrorMessage()); } } - return exceptionForGeneralError(response); + return exceptionForGeneralError(result); }); - return response.readEntity(XMLDirectSignatureJobStatusResponse.class); + return parseResponse(result, XMLDirectSignatureJobStatusResponse.class); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new UncheckedIOException(e); } }); } public ResponseInputStream getDataStream(URI uri, MediaType ... acceptedResponses) { - return getDataStream(ignoredRoot -> httpClient.target(uri), acceptedResponses); - } - - public ResponseInputStream getDataStream(UnaryOperator targetResolver, MediaType ... acceptedResponses) { return call(() -> { - Response response = targetResolver.apply(httpClient.signatureServiceRoot()).request().accept(acceptedResponses).get(); - InputStream inputStream = parseResponse(response, InputStream.class); - return new ResponseInputStream(inputStream, response.getLength()); + var requestBuilder = HttpRequest.newBuilder() + .uri(uri) + .GET(); + + Arrays.stream(acceptedResponses).forEach(mediaType -> { + requestBuilder.header(ACCEPT, mediaType.getType()); + }); + + try { + HttpResponse response = httpClient.httpClient().send(requestBuilder.build(), BodyHandlers.ofInputStream()); + ResponseStatus.resolve(response.statusCode()).expect(SUCCESSFUL).orThrow(unexpectedStatus -> exceptionForGeneralError(response)); + // TODO: Hvordan bør det løses med content_length? + return new ResponseInputStream(response.body(), Integer.parseInt(response.headers().firstValue(CONTENT_LENGTH).orElseThrow())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } }); } public void cancel(final Cancellable cancellable) { call(() -> { if (cancellable.getCancellationUrl() != null) { - URI url = cancellable.getCancellationUrl().getUrl(); - try (Response response = postEmptyEntity(url)) { - ResponseStatus.resolve(response.getStatus()) + var response = postEmptyEntity(cancellable.getCancellationUrl().getUrl()); + ResponseStatus.resolve(response.statusCode()) .throwIf(CONFLICT, status -> new JobCannotBeCancelledException(status, extractError(response))) .expect(SUCCESSFUL) .orThrow(status -> exceptionForGeneralError(response)); - } } else { throw new NotCancellableException(); } @@ -176,21 +239,29 @@ public JobStatusResponse getDirectStatusCha private JobStatusResponse getStatusChange(final Optional sender, final Target target, final Class responseClass) { return call(() -> { Sender actualSender = getActualSender(sender, globalSender); - Invocation.Builder request = httpClient.signatureServiceRoot().path(target.path(actualSender)) - .queryParam(POLLING_QUEUE_QUERY_PARAMETER, actualSender.getPollingQueue().value) - .request() - .accept(APPLICATION_XML_TYPE); - try (Response response = request.get()) { - StatusType status = ResponseStatus.resolve(response.getStatus()) + var request = HttpRequest.newBuilder() + .uri(UriBuilder.addQuery(httpClient.signatureServiceRoot().resolve(target.path(actualSender)), POLLING_QUEUE_QUERY_PARAMETER + "=" + actualSender.getPollingQueue().value)) + .header(ACCEPT, APPLICATION_XML_TYPE.getType()) + .GET() + .build(); + try { + HttpResponse response = httpClient.httpClient().send(request, BodyHandlers.ofInputStream()); + StatusType status = ResponseStatus.resolve(response.statusCode()) .throwIf(TOO_MANY_REQUESTS, s -> new TooEagerPollingException()) .expect(SUCCESSFUL).orThrow(unexpectedStatus -> exceptionForGeneralError(response)); - return new JobStatusResponse<>(status == NO_CONTENT ? null : response.readEntity(responseClass), getNextPermittedPollTime(response)); + return new JobStatusResponse<>(status == NO_CONTENT ? null : Marshalling.unmarshal(response.body(), responseClass), getNextPermittedPollTime(response)); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); } }); } - private static Instant getNextPermittedPollTime(Response response) { - return ZonedDateTime.parse(response.getHeaderString(NEXT_PERMITTED_POLL_TIME_HEADER), ISO_DATE_TIME).toInstant(); + private static Instant getNextPermittedPollTime(HttpResponse response) { + return response.headers().firstValue(NEXT_PERMITTED_POLL_TIME_HEADER) + .map(nextPermittedPollTime -> ZonedDateTime.parse(nextPermittedPollTime, ISO_DATE_TIME).toInstant()) + .orElseThrow(() -> new RuntimeException("Expected header " + NEXT_PERMITTED_POLL_TIME_HEADER + " to exist")); } public void confirm(final Confirmable confirmable) { @@ -198,9 +269,8 @@ public void confirm(final Confirmable confirmable) { if (confirmable.getConfirmationReference() != null) { URI url = confirmable.getConfirmationReference().getConfirmationUrl(); LOG.info("Sends confirmation for '{}' to URL {}", confirmable, url); - try (Response response = postEmptyEntity(url)) { - ResponseStatus.resolve(response.getStatus()).expect(SUCCESSFUL).orThrow(status -> exceptionForGeneralError(response)); - } + var response = postEmptyEntity(url); + ResponseStatus.resolve(response.statusCode()).expect(SUCCESSFUL).orThrow(status -> exceptionForGeneralError(response)); } else { LOG.info("Does not need to send confirmation for '{}'", confirmable); } @@ -218,98 +288,94 @@ private void call(Runnable action) { public void deleteDocuments(DeleteDocumentsUrl deleteDocumentsUrl) { call(() -> { if (deleteDocumentsUrl != null) { - URI url = deleteDocumentsUrl.getUrl(); - try (Response response = delete(url)) { - ResponseStatus.resolve(response.getStatus()).expect(SUCCESSFUL).orThrow(status -> exceptionForGeneralError(response)); - } + var url = deleteDocumentsUrl.getUrl(); + var response = delete(url); + ResponseStatus.resolve(response.statusCode()).expect(SUCCESSFUL).orThrow(status -> exceptionForGeneralError(response)); } else { throw new DocumentsNotDeletableException(); } }); } - - private class UsingBodyParts { - - private final List parts; - - UsingBodyParts(BodyPart... parts) { - this.parts = Arrays.asList(parts); - } - - T postAsMultiPart(String path, Class responseType) { - try (MultiPart multiPart = new MultiPart()) { - for (BodyPart bodyPart : parts) { - multiPart.bodyPart(bodyPart); - } - - Invocation.Builder request = httpClient.signatureServiceRoot().path(path) - .request() - .header(CONTENT_TYPE, multiPart.getMediaType()) - .accept(APPLICATION_XML_TYPE); - try (Response response = request.post(Entity.entity(multiPart, multiPart.getMediaType()))) { - return parseResponse(response, responseType); - } - } catch (IOException e) { - throw new RuntimeIOException(e); - } + private HttpResponse postEmptyEntity(URI uri) { + try { + var request = HttpRequest.newBuilder() + .uri(uri) + .header(ACCEPT, APPLICATION_XML_TYPE.getType()) + .POST(HttpRequest.BodyPublishers.noBody()) + .build(); + + return httpClient.httpClient().send(request, BodyHandlers.ofInputStream()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); } } - private Response postEmptyEntity(URI uri) { - return postEntity(uri, null); - } - - private Response postEntity(URI uri, Object entity) { - Invocation.Builder requestBuilder = httpClient.target(uri) - .request() - .accept(APPLICATION_XML_TYPE); - return (entity == null ? requestBuilder.header(CONTENT_LENGTH, 0) : requestBuilder) - .post(Entity.entity(entity, APPLICATION_XML_TYPE)); - } - - - private Response delete(URI uri) { - return httpClient.target(uri) - .request() - .accept(APPLICATION_XML_TYPE) - .delete(); + private HttpResponse delete(URI uri) { + try { + var request = HttpRequest.newBuilder() + .uri(uri) + .header(ACCEPT, APPLICATION_XML_TYPE.getType()) + .DELETE() + .build(); + + return httpClient.httpClient().send(request, BodyHandlers.ofInputStream()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } } - private static T parseResponse(Response response, Class responseType) { - ResponseStatus.resolve(response.getStatus()).expect(SUCCESSFUL).orThrow(unexpectedStatus -> exceptionForGeneralError(response)); - return response.readEntity(responseType); + private static T parseResponse(HttpResponse response, Class responseType) { + ResponseStatus.resolve(response.statusCode()).expect(SUCCESSFUL).orThrow(unexpectedStatus -> exceptionForGeneralError(response)); + try(var body = response.body()) { + return Marshalling.unmarshal(body, responseType); + } catch (IOException e) { + throw new UncheckedIOException("Could not parse response.", e); + } } - private static SignatureException exceptionForGeneralError(Response response) { + private static SignatureException exceptionForGeneralError(HttpResponse response) { XMLError error = extractError(response); if (BROKER_NOT_AUTHORIZED.sameAs(error.getErrorCode())) { return new BrokerNotAuthorizedException(error); } - return new UnexpectedResponseException(error, ResponseStatus.resolve(response.getStatus()).get(), OK); + return new UnexpectedResponseException(error, ResponseStatus.resolve(response.statusCode()).get(), OK); } - private static XMLError extractError(Response response) { + private static XMLError extractError(HttpResponse response) { XMLError error; - Optional responseContentType = Optional.ofNullable(response.getHeaderString(HttpHeaders.CONTENT_TYPE)); + Optional responseContentType = response.headers().firstValue(HttpHeaders.CONTENT_TYPE); if (responseContentType.isPresent() && MediaType.valueOf(responseContentType.get()).equals(APPLICATION_XML_TYPE)) { - try { - response.bufferEntity(); - error = response.readEntity(XMLError.class); - } catch (Exception e) { - throw new UnexpectedResponseException( - HttpHeaders.CONTENT_TYPE + " " + responseContentType.orElse("unknown") + ": " + - Optional.ofNullable(response.readEntity(String.class)).filter(StringUtils::isNoneBlank).orElse(""), - e, ResponseStatus.resolve(response.getStatus()).get(), OK); + try(var body = response.body()) { + error = Marshalling.unmarshal(body, XMLError.class); + } catch (IOException e) { + throw new UncheckedIOException("Could not extract error from body.", e); } } else { + String errorAsString; + try(var body = response.body()) { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + for (int length; (length = body.read(buffer)) != -1; ) { + result.write(buffer, 0, length); + } + errorAsString = result.toString(StandardCharsets.UTF_8); + } catch (IOException e) { + // TODO: Kan tolke dette som en empty errorAsString? Dvs ignorere feilen? + throw new UncheckedIOException("Could not read body as string.", e); + } throw new UnexpectedResponseException( HttpHeaders.CONTENT_TYPE + " " + responseContentType.orElse("unknown") + ": " + - Optional.ofNullable(response.readEntity(String.class)).filter(StringUtils::isNoneBlank).orElse(""), - ResponseStatus.resolve(response.getStatus()).get(), OK); + Optional.ofNullable(errorAsString).filter(StringUtils::isNoneBlank).orElse(""), + ResponseStatus.resolve(response.statusCode()).get(), OK); } + // TODO: Dette skjer vel bare hvis vi ignorere IOExceptions? if (error == null) { - throw new UnexpectedResponseException(null, ResponseStatus.resolve(response.getStatus()).get(), OK); + throw new UnexpectedResponseException(null, ResponseStatus.resolve(response.statusCode()).get(), OK); } return error; } diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/AddRequestHeaderFilter.java b/src/main/java/no/digipost/signature/client/core/internal/http/AddRequestHeaderFilter.java index 8132be46..9f7c60af 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/http/AddRequestHeaderFilter.java +++ b/src/main/java/no/digipost/signature/client/core/internal/http/AddRequestHeaderFilter.java @@ -1,11 +1,11 @@ package no.digipost.signature.client.core.internal.http; -import javax.annotation.Priority; -import javax.ws.rs.client.ClientRequestContext; -import javax.ws.rs.client.ClientRequestFilter; +import jakarta.annotation.Priority; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; import java.io.IOException; -import static javax.ws.rs.Priorities.HEADER_DECORATOR; +import static jakarta.ws.rs.Priorities.HEADER_DECORATOR; @Priority(HEADER_DECORATOR) diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/HttpIntegrationConfiguration.java b/src/main/java/no/digipost/signature/client/core/internal/http/HttpIntegrationConfiguration.java index e13a9af7..983218b8 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/http/HttpIntegrationConfiguration.java +++ b/src/main/java/no/digipost/signature/client/core/internal/http/HttpIntegrationConfiguration.java @@ -1,18 +1,15 @@ package no.digipost.signature.client.core.internal.http; -import javax.net.ssl.SSLContext; -import javax.ws.rs.core.Configuration; - import java.net.URI; +import java.net.http.HttpClient; +import java.time.Duration; public interface HttpIntegrationConfiguration { - String PRE_INIT_CLIENT = "no.posten.signering.client.preInit"; - - Configuration getJaxrsConfiguration(); - - SSLContext getSSLContext(); + HttpClient httpClient(); URI getServiceRoot(); + Duration socketTimeout(); + } diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/ResponseStatus.java b/src/main/java/no/digipost/signature/client/core/internal/http/ResponseStatus.java index fb30e0ce..be80ead7 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/http/ResponseStatus.java +++ b/src/main/java/no/digipost/signature/client/core/internal/http/ResponseStatus.java @@ -2,9 +2,9 @@ import no.digipost.signature.client.core.exceptions.UnexpectedResponseException; -import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.Response.Status.Family; -import javax.ws.rs.core.Response.StatusType; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.Response.Status.Family; +import jakarta.ws.rs.core.Response.StatusType; import java.util.Objects; import java.util.function.BiPredicate; diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClient.java b/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClient.java index 3a6761cc..87178f2a 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClient.java +++ b/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClient.java @@ -1,13 +1,15 @@ package no.digipost.signature.client.core.internal.http; -import javax.ws.rs.client.WebTarget; - import java.net.URI; +import java.net.http.HttpClient; +import java.time.Duration; public interface SignatureHttpClient { - WebTarget signatureServiceRoot(); + URI signatureServiceRoot(); + + HttpClient httpClient(); - WebTarget target(URI uri); + Duration socketTimeout(); } diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClientFactory.java b/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClientFactory.java index e0a638ae..bde42d39 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClientFactory.java +++ b/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClientFactory.java @@ -1,78 +1,46 @@ package no.digipost.signature.client.core.internal.http; -import org.apache.http.conn.ssl.NoopHostnameVerifier; -import org.glassfish.jersey.client.JerseyClient; -import org.glassfish.jersey.client.JerseyClientBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.ws.rs.client.Client; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.Configuration; - import java.net.URI; - -import static java.lang.Boolean.parseBoolean; -import static no.digipost.signature.client.core.internal.http.HttpIntegrationConfiguration.PRE_INIT_CLIENT; +import java.net.http.HttpClient; +import java.time.Duration; public class SignatureHttpClientFactory { private static final Logger LOG = LoggerFactory.getLogger(SignatureHttpClientFactory.class); public static SignatureHttpClient create(HttpIntegrationConfiguration config) { - Configuration jaxrsConfiguration = config.getJaxrsConfiguration(); - JerseyClientBuilder jerseyBuilder = (JerseyClientBuilder) JerseyClientBuilder.newBuilder(); - JerseyClient jerseyClient = jerseyBuilder - .withConfig(jaxrsConfiguration) - .sslContext(config.getSSLContext()) - .hostnameVerifier(NoopHostnameVerifier.INSTANCE) - .build(); - - Object configuredPreInit = jaxrsConfiguration.getProperty(PRE_INIT_CLIENT); - if (configuredPreInit == null || parseBoolean(String.valueOf(configuredPreInit))) { - try { - jerseyClient.preInitialize(); - } catch (Exception e) { - throw new IllegalStateException( - "Unable to pre-initialize Jersey Client, because " + e.getClass().getSimpleName() + " '" + e.getMessage() + "'. " + - "This step is taken to ensure everything the client needs is available already on instantiation, " + - "in particular the InjectionManager facilities used internally by Jersey. By default, the Signature API Client " + - "should include the Jersey HK2 implementation, but if you need to control this yourself, consider excluding " + - "org.glassfish.jersey.inject:jersey-hk2 when depending on the signature-api-client-java, and make sure to make the " + - "InjectionManagerFactory of your choice discoverable by Jersey.", e); - } - } else { - LOG.warn( - "Pre-initializing the Signature API Jersey Client is disabled, because of the property " + - PRE_INIT_CLIENT + "=" + configuredPreInit + " is set in the JAX-RS configuration. " + - "There is a chance that requests done by Jersey Client will break, and we don't yet know about it. " + - "If this is intended configured behavior, this warning may be ignored."); - } - return new DefaultClient(jerseyClient, config.getServiceRoot()); + return new DefaultClient(config.httpClient(), config.getServiceRoot(), config.socketTimeout()); } private static final class DefaultClient implements SignatureHttpClient { - private final Client jerseyClient; - private final WebTarget signatureServiceRoot; + private final HttpClient httpClient; + private final URI signatureServiceRoot; + private final Duration socketTimeout; - DefaultClient(Client jerseyClient, URI root) { - this.jerseyClient = jerseyClient; - this.signatureServiceRoot = jerseyClient.target(root); + DefaultClient(HttpClient httpClient, URI root, Duration socketTimeout) { + this.httpClient = httpClient; + this.signatureServiceRoot = root; + this.socketTimeout = socketTimeout; } @Override - public WebTarget target(URI uri) { - return jerseyClient.target(uri); + public URI signatureServiceRoot() { + return signatureServiceRoot; } - @Override - public WebTarget signatureServiceRoot() { - return signatureServiceRoot; + public HttpClient httpClient() { + return httpClient; } + public Duration socketTimeout() { + return socketTimeout; + } } } diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/UriHelper.java b/src/main/java/no/digipost/signature/client/core/internal/http/UriHelper.java new file mode 100644 index 00000000..fb663b2d --- /dev/null +++ b/src/main/java/no/digipost/signature/client/core/internal/http/UriHelper.java @@ -0,0 +1,22 @@ +package no.digipost.signature.client.core.internal.http; + +import java.net.URI; +import java.net.URISyntaxException; + +public class UriBuilder { + + public static URI addQuery(URI uri, String query) { + String newQuery = uri.getQuery(); + if (newQuery == null) { + newQuery = query; + } else { + newQuery += "&" + query; + } + + try { + return new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), newQuery, uri.getFragment()); + } catch (URISyntaxException e) { + throw new RuntimeException("Could not append query [" + query + "] to uri [" + uri + "]", e); + } + } +} diff --git a/src/main/java/no/digipost/signature/client/core/internal/xml/JaxbMessageReaderWriterProvider.java b/src/main/java/no/digipost/signature/client/core/internal/xml/JaxbMessageReaderWriterProvider.java deleted file mode 100644 index 10f7dce6..00000000 --- a/src/main/java/no/digipost/signature/client/core/internal/xml/JaxbMessageReaderWriterProvider.java +++ /dev/null @@ -1,44 +0,0 @@ -package no.digipost.signature.client.core.internal.xml; - -import org.glassfish.jersey.message.internal.AbstractMessageReaderWriterProvider; - -import javax.ws.rs.Consumes; -import javax.ws.rs.Produces; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.ext.Provider; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -import static no.digipost.signature.client.core.internal.xml.Marshalling.marshal; -import static no.digipost.signature.client.core.internal.xml.Marshalling.unmarshal; - -@Provider -@Produces(MediaType.APPLICATION_XML) -@Consumes(MediaType.APPLICATION_XML) -public class JaxbMessageReaderWriterProvider extends AbstractMessageReaderWriterProvider { - - @Override - public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { - return mediaType.isCompatible(MediaType.APPLICATION_XML_TYPE); - } - - @Override - public Object readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { - return unmarshal(entityStream); - } - - @Override - public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { - return mediaType.isCompatible(MediaType.APPLICATION_XML_TYPE); - } - - @Override - public void writeTo(Object o, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { - marshal(o, entityStream); - } -} diff --git a/src/main/java/no/digipost/signature/client/core/internal/xml/Marshalling.java b/src/main/java/no/digipost/signature/client/core/internal/xml/Marshalling.java index 81d33bc2..4ece53f4 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/xml/Marshalling.java +++ b/src/main/java/no/digipost/signature/client/core/internal/xml/Marshalling.java @@ -1,20 +1,18 @@ package no.digipost.signature.client.core.internal.xml; -import no.digipost.signature.jaxb.spring.SignatureJaxb2Marshaller; +import no.digipost.signature.jaxb.JaxbMarshaller; -import javax.xml.transform.stream.StreamResult; -import javax.xml.transform.stream.StreamSource; import java.io.InputStream; import java.io.OutputStream; public final class Marshalling { public static void marshal(Object object, OutputStream entityStream) { - SignatureJaxb2Marshaller.ForRequestsOfAllApis.singleton().marshal(object, new StreamResult(entityStream)); + JaxbMarshaller.ForRequestsOfAllApis.singleton().marshal(object, entityStream); } - public static Object unmarshal(InputStream entityStream) { - return SignatureJaxb2Marshaller.ForResponsesOfAllApis.singleton().unmarshal(new StreamSource(entityStream)); + public static T unmarshal(InputStream entityStream, Class type) { + return JaxbMarshaller.ForResponsesOfAllApis.singleton().unmarshal(entityStream, type); } private Marshalling() { } diff --git a/src/main/java/no/digipost/signature/client/direct/DirectClient.java b/src/main/java/no/digipost/signature/client/direct/DirectClient.java index fef8dae7..d536bd0a 100644 --- a/src/main/java/no/digipost/signature/client/direct/DirectClient.java +++ b/src/main/java/no/digipost/signature/client/direct/DirectClient.java @@ -20,8 +20,8 @@ import java.util.Optional; -import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE; -import static javax.ws.rs.core.MediaType.APPLICATION_XML_TYPE; +import static jakarta.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE; +import static jakarta.ws.rs.core.MediaType.APPLICATION_XML_TYPE; import static no.digipost.signature.client.direct.DirectJobStatusResponse.noUpdatedStatus; import static no.digipost.signature.client.direct.JaxbEntityMapping.fromJaxb; import static no.digipost.signature.client.direct.JaxbEntityMapping.toJaxb; diff --git a/src/main/java/no/digipost/signature/client/direct/StatusReference.java b/src/main/java/no/digipost/signature/client/direct/StatusReference.java index ccb78ede..58d3b5d2 100644 --- a/src/main/java/no/digipost/signature/client/direct/StatusReference.java +++ b/src/main/java/no/digipost/signature/client/direct/StatusReference.java @@ -1,6 +1,6 @@ package no.digipost.signature.client.direct; -import javax.ws.rs.core.UriBuilder; +import no.digipost.signature.client.core.internal.http.UriBuilder; import java.net.URI; import java.util.Objects; @@ -54,7 +54,7 @@ private StatusReference(URI statusUrl, String statusQueryToken) { } public URI getStatusUrl() { - return UriBuilder.fromUri(statusUrl).queryParam(STATUS_QUERY_TOKEN_PARAM_NAME, statusQueryToken).build(); + return UriBuilder.addQuery(statusUrl,STATUS_QUERY_TOKEN_PARAM_NAME + "=" + statusQueryToken); } public static abstract class StatusUrlContruction { diff --git a/src/main/java/no/digipost/signature/client/portal/PortalClient.java b/src/main/java/no/digipost/signature/client/portal/PortalClient.java index 84202baa..7206d64a 100644 --- a/src/main/java/no/digipost/signature/client/portal/PortalClient.java +++ b/src/main/java/no/digipost/signature/client/portal/PortalClient.java @@ -20,8 +20,8 @@ import java.util.Optional; -import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE; -import static javax.ws.rs.core.MediaType.APPLICATION_XML_TYPE; +import static jakarta.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE; +import static jakarta.ws.rs.core.MediaType.APPLICATION_XML_TYPE; import static no.digipost.signature.client.portal.JaxbEntityMapping.fromJaxb; import static no.digipost.signature.client.portal.JaxbEntityMapping.toJaxb; import static no.digipost.signature.client.portal.PortalJobStatusChanged.noUpdatedStatus; diff --git a/src/test/java/no/digipost/signature/client/asice/signature/CreateSignatureTest.java b/src/test/java/no/digipost/signature/client/asice/signature/CreateSignatureTest.java index 918d6842..327fe8c0 100644 --- a/src/test/java/no/digipost/signature/client/asice/signature/CreateSignatureTest.java +++ b/src/test/java/no/digipost/signature/client/asice/signature/CreateSignatureTest.java @@ -15,8 +15,11 @@ import no.digipost.signature.client.security.KeyStoreConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.oxm.jaxb.Jaxb2Marshaller; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; import javax.xml.transform.stream.StreamSource; import java.io.ByteArrayInputStream; @@ -46,9 +49,20 @@ public class CreateSignatureTest { private KeyStoreConfig noekkelpar; private List files; - private static final Jaxb2Marshaller marshaller; static { - marshaller = new Jaxb2Marshaller(); - marshaller.setClassesToBeBound(XAdESSignatures.class, QualifyingProperties.class); + private static final Marshaller marshaller; static { + try { + marshaller = JAXBContext.newInstance(XAdESSignatures.class, QualifyingProperties.class).createMarshaller(); + } catch (JAXBException e) { + throw new RuntimeException(e); + } + } + + private static final Unmarshaller unmarshaller; static { + try { + unmarshaller = JAXBContext.newInstance(XAdESSignatures.class, QualifyingProperties.class).createUnmarshaller(); + } catch (JAXBException e) { + throw new RuntimeException(e); + } } @BeforeEach @@ -64,7 +78,7 @@ public void setUp() { } @Test - public void test_generated_signatures() { + public void test_generated_signatures() throws JAXBException { /* * Expected signature value (Base-64 encoded) from the given keys, files, and time of the signing. * If this changes, something has changed in the signature implementation, and must be investigated! @@ -76,7 +90,7 @@ public void test_generated_signatures() { "PAdNndCACJiWrkHNQ5gTJ+UWx8y2kuzZHEGGTJ+ip9KpCRohDfLapQAMTh0zMLrUNbYpq6kiYWrlxTNfdcVm4skBY0j9Q=="; Signature signature = createSignature.createSignature(files, noekkelpar); - XAdESSignatures xAdESSignatures = (XAdESSignatures) marshaller.unmarshal(new StreamSource(new ByteArrayInputStream(signature.getContent()))); + XAdESSignatures xAdESSignatures = (XAdESSignatures) unmarshaller.unmarshal(new StreamSource(new ByteArrayInputStream(signature.getContent()))); assertThat(xAdESSignatures, where(XAdESSignatures::getSignatures, hasSize(1))); no.digipost.signature.api.xml.thirdparty.xmldsig.Signature dSignature = xAdESSignatures.getSignatures().get(0); @@ -86,10 +100,10 @@ public void test_generated_signatures() { } @Test - public void test_xades_signed_properties() { + public void test_xades_signed_properties() throws JAXBException { Signature signature = createSignature.createSignature(files, noekkelpar); - XAdESSignatures xAdESSignatures = (XAdESSignatures) marshaller.unmarshal(new StreamSource(new ByteArrayInputStream(signature.getContent()))); + XAdESSignatures xAdESSignatures = (XAdESSignatures) unmarshaller.unmarshal(new StreamSource(new ByteArrayInputStream(signature.getContent()))); no.digipost.signature.api.xml.thirdparty.xmldsig.Object object = xAdESSignatures.getSignatures().get(0).getObjects().get(0); QualifyingProperties xadesProperties = (QualifyingProperties) object.getContent().get(0); @@ -101,14 +115,14 @@ public void test_xades_signed_properties() { } @Test - public void should_support_filenames_with_spaces_and_other_characters() { + public void should_support_filenames_with_spaces_and_other_characters() throws JAXBException { List otherFiles = asList( file("dokument (2).pdf", "hoveddokument-innhold".getBytes(), DocumentType.PDF.getMediaType()), file("manifest.xml", "manifest-innhold".getBytes(), ASiCEAttachable.XML_MEDIATYPE) ); Signature signature = createSignature.createSignature(otherFiles, noekkelpar); - XAdESSignatures xAdESSignatures = (XAdESSignatures) marshaller.unmarshal(new StreamSource(new ByteArrayInputStream(signature.getContent()))); + XAdESSignatures xAdESSignatures = (XAdESSignatures) unmarshaller.unmarshal(new StreamSource(new ByteArrayInputStream(signature.getContent()))); String uri = xAdESSignatures.getSignatures().get(0).getSignedInfo().getReferences().get(0).getURI(); assertThat(uri, is("dokument+%282%29.pdf")); } diff --git a/src/test/java/no/digipost/signature/client/core/internal/http/ResponseStatusTest.java b/src/test/java/no/digipost/signature/client/core/internal/http/ResponseStatusTest.java index 3c9faf83..032a6896 100644 --- a/src/test/java/no/digipost/signature/client/core/internal/http/ResponseStatusTest.java +++ b/src/test/java/no/digipost/signature/client/core/internal/http/ResponseStatusTest.java @@ -6,7 +6,7 @@ import no.digipost.signature.client.core.internal.http.ResponseStatus.Unknown; import org.junit.jupiter.api.Test; -import javax.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.Response.Status; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; diff --git a/src/test/java/no/digipost/signature/client/core/internal/http/SignatureHttpClientFactoryTest.java b/src/test/java/no/digipost/signature/client/core/internal/http/SignatureHttpClientFactoryTest.java index d6ef4f8d..c62e8a5f 100644 --- a/src/test/java/no/digipost/signature/client/core/internal/http/SignatureHttpClientFactoryTest.java +++ b/src/test/java/no/digipost/signature/client/core/internal/http/SignatureHttpClientFactoryTest.java @@ -1,46 +1,36 @@ package no.digipost.signature.client.core.internal.http; -import org.glassfish.jersey.client.ClientConfig; import org.junit.jupiter.api.Test; -import javax.net.ssl.SSLContext; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.Configuration; - import java.net.URI; -import java.security.NoSuchAlgorithmException; +import java.net.http.HttpClient; +import java.time.Duration; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import static uk.co.probablyfine.matchers.Java8Matchers.where; public class SignatureHttpClientFactoryTest implements HttpIntegrationConfiguration { @Test void instantiatesSignatureHttpClient() { SignatureHttpClient signatureHttpClient = SignatureHttpClientFactory.create(this); - WebTarget target = signatureHttpClient.signatureServiceRoot(); - assertThat(target, where(WebTarget::getUri, is(this.getServiceRoot()))); + URI root = signatureHttpClient.signatureServiceRoot(); + assertThat(root, is(this.getServiceRoot())); } - @Override - public Configuration getJaxrsConfiguration() { - return new ClientConfig(); + public HttpClient httpClient() { + return HttpClient.newHttpClient(); } @Override - public SSLContext getSSLContext() { - try { - return SSLContext.getDefault(); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e.getMessage(), e); - } + public URI getServiceRoot() { + return URI.create("localhost"); } @Override - public URI getServiceRoot() { - return URI.create("localhost"); + public Duration socketTimeout() { + return Duration.ofSeconds(10); } } diff --git a/src/test/java/no/digipost/signature/client/core/internal/http/UriHelperTest.java b/src/test/java/no/digipost/signature/client/core/internal/http/UriHelperTest.java new file mode 100644 index 00000000..d5617ed3 --- /dev/null +++ b/src/test/java/no/digipost/signature/client/core/internal/http/UriHelperTest.java @@ -0,0 +1,25 @@ +package no.digipost.signature.client.core.internal.http; + +import org.junit.jupiter.api.Test; + +import java.net.URI; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class UriBuilderTest { + + @Test + void appendsQueryParam() { + var uri = URI.create("https://test.example.com"); + var withQuery = UriBuilder.addQuery(uri, "key=value"); + assertThat(withQuery, is(URI.create("https://test.example.com?key=value"))); + } + + @Test + void appendsQueryOnUriWithQuery() { + var uri = URI.create("https://test.example.com?key1=value1"); + var withQuery = UriBuilder.addQuery(uri, "key2=value2"); + assertThat(withQuery, is(URI.create("https://test.example.com?key1=value1&key2=value2"))); + } +} From 0fd78f2524e87e284766e659bde5e9e6aa850349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20St=C3=B8a?= Date: Wed, 8 Feb 2023 09:38:11 +0100 Subject: [PATCH 02/32] rename UriBuilder to UriHelper and add som tests --- .../signature/client/core/internal/ClientHelper.java | 4 ++-- .../signature/client/core/internal/http/UriHelper.java | 2 +- .../digipost/signature/client/direct/StatusReference.java | 4 ++-- .../signature/client/core/internal/http/UriHelperTest.java | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java index 86133e26..1a7d8289 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java @@ -25,7 +25,7 @@ import no.digipost.signature.client.core.exceptions.UnexpectedResponseException; import no.digipost.signature.client.core.internal.http.ResponseStatus; import no.digipost.signature.client.core.internal.http.SignatureHttpClient; -import no.digipost.signature.client.core.internal.http.UriBuilder; +import no.digipost.signature.client.core.internal.http.UriHelper; import no.digipost.signature.client.core.internal.xml.Marshalling; import no.digipost.signature.client.direct.WithSignerUrl; import org.apache.commons.lang3.StringUtils; @@ -240,7 +240,7 @@ private JobStatusResponse getStatusChange(final return call(() -> { Sender actualSender = getActualSender(sender, globalSender); var request = HttpRequest.newBuilder() - .uri(UriBuilder.addQuery(httpClient.signatureServiceRoot().resolve(target.path(actualSender)), POLLING_QUEUE_QUERY_PARAMETER + "=" + actualSender.getPollingQueue().value)) + .uri(UriHelper.addQuery(httpClient.signatureServiceRoot().resolve(target.path(actualSender)), POLLING_QUEUE_QUERY_PARAMETER + "=" + actualSender.getPollingQueue().value)) .header(ACCEPT, APPLICATION_XML_TYPE.getType()) .GET() .build(); diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/UriHelper.java b/src/main/java/no/digipost/signature/client/core/internal/http/UriHelper.java index fb663b2d..906446c7 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/http/UriHelper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/http/UriHelper.java @@ -3,7 +3,7 @@ import java.net.URI; import java.net.URISyntaxException; -public class UriBuilder { +public class UriHelper { public static URI addQuery(URI uri, String query) { String newQuery = uri.getQuery(); diff --git a/src/main/java/no/digipost/signature/client/direct/StatusReference.java b/src/main/java/no/digipost/signature/client/direct/StatusReference.java index 58d3b5d2..eaac236d 100644 --- a/src/main/java/no/digipost/signature/client/direct/StatusReference.java +++ b/src/main/java/no/digipost/signature/client/direct/StatusReference.java @@ -1,6 +1,6 @@ package no.digipost.signature.client.direct; -import no.digipost.signature.client.core.internal.http.UriBuilder; +import no.digipost.signature.client.core.internal.http.UriHelper; import java.net.URI; import java.util.Objects; @@ -54,7 +54,7 @@ private StatusReference(URI statusUrl, String statusQueryToken) { } public URI getStatusUrl() { - return UriBuilder.addQuery(statusUrl,STATUS_QUERY_TOKEN_PARAM_NAME + "=" + statusQueryToken); + return UriHelper.addQuery(statusUrl,STATUS_QUERY_TOKEN_PARAM_NAME + "=" + statusQueryToken); } public static abstract class StatusUrlContruction { diff --git a/src/test/java/no/digipost/signature/client/core/internal/http/UriHelperTest.java b/src/test/java/no/digipost/signature/client/core/internal/http/UriHelperTest.java index d5617ed3..e3a36b63 100644 --- a/src/test/java/no/digipost/signature/client/core/internal/http/UriHelperTest.java +++ b/src/test/java/no/digipost/signature/client/core/internal/http/UriHelperTest.java @@ -7,19 +7,19 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -public class UriBuilderTest { +public class UriHelperTest { @Test void appendsQueryParam() { var uri = URI.create("https://test.example.com"); - var withQuery = UriBuilder.addQuery(uri, "key=value"); + var withQuery = UriHelper.addQuery(uri, "key=value"); assertThat(withQuery, is(URI.create("https://test.example.com?key=value"))); } @Test void appendsQueryOnUriWithQuery() { var uri = URI.create("https://test.example.com?key1=value1"); - var withQuery = UriBuilder.addQuery(uri, "key2=value2"); + var withQuery = UriHelper.addQuery(uri, "key2=value2"); assertThat(withQuery, is(URI.create("https://test.example.com?key1=value1&key2=value2"))); } } From db4c528d7f9861d87c7864200880058b6f425431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20St=C3=B8a?= Date: Wed, 8 Feb 2023 09:38:44 +0100 Subject: [PATCH 03/32] describe exception when CONTENT_LENGTH header is not present --- .../signature/client/core/internal/ClientHelper.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java index 1a7d8289..b1a7d195 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java @@ -204,8 +204,11 @@ public ResponseInputStream getDataStream(URI uri, MediaType ... acceptedResponse try { HttpResponse response = httpClient.httpClient().send(requestBuilder.build(), BodyHandlers.ofInputStream()); ResponseStatus.resolve(response.statusCode()).expect(SUCCESSFUL).orThrow(unexpectedStatus -> exceptionForGeneralError(response)); - // TODO: Hvordan bør det løses med content_length? - return new ResponseInputStream(response.body(), Integer.parseInt(response.headers().firstValue(CONTENT_LENGTH).orElseThrow())); + return new ResponseInputStream( + response.body(), + Integer.parseInt(response.headers() + .firstValue(CONTENT_LENGTH) + .orElseThrow(() -> new RuntimeException("Expected header " + CONTENT_LENGTH + " to exist")))); } catch (IOException e) { throw new UncheckedIOException(e); } catch (InterruptedException e) { From f5e9a40b94cbc8e9b4c45e23bd899ff4207b005e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20St=C3=B8a?= Date: Wed, 8 Feb 2023 13:59:19 +0100 Subject: [PATCH 04/32] use signature api version remove-spring-dependency-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a35609dc..cfc2a1ed 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ 11 11 set_with_-Dproject.previousVersion=X.Y - LOCAL-SNAPSHOT + remove-spring-dependency-SNAPSHOT 1.7.36 From 65bb515b6ee0e4cf41d6768e87c54949fd2bbd43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20St=C3=B8a?= Date: Wed, 8 Feb 2023 16:11:09 +0100 Subject: [PATCH 05/32] update notice and upgrade some dependencies --- NOTICE | 22 +--------------------- pom.xml | 37 +++++++++---------------------------- 2 files changed, 10 insertions(+), 49 deletions(-) diff --git a/NOTICE b/NOTICE index 6c844b4e..9c6c81fe 100644 --- a/NOTICE +++ b/NOTICE @@ -8,44 +8,24 @@ Licensed under Apache 2 - http://www.apache.org/licenses/LICENSE-2.0.html This software includes third party software subject to the following licenses: - aopalliance version 1.0 repackaged as a module under EPL 2.0 or GPL2 w/ CPE Apache Commons Codec under Apache License, Version 2.0 Apache Commons Lang under Apache License, Version 2.0 Apache HttpClient under Apache License, Version 2.0 Apache HttpCore under Apache License, Version 2.0 Digipost Certificate Validator under The Apache Software License, Version 2.0 - HK2 API module under EPL 2.0 or GPL2 w/ CPE - HK2 Implementation Utilities under EPL 2.0 or GPL2 w/ CPE istack common utility code runtime under Eclipse Distribution License - v 1.0 Jakarta Activation under EDL 1.0 Jakarta Activation API jar under EDL 1.0 Jakarta Annotations API under EPL 2.0 or GPL2 w/ CPE + Jakarta RESTful WS API under EPL-2.0 or GPL-2.0-with-classpath-exception Jakarta XML Binding API under Eclipse Distribution License - v 1.0 - jakarta.ws.rs-api under EPL 2.0 or GPL2 w/ CPE - Javassist under MPL 1.1 or LGPL 2.1 or Apache License 2.0 - javax.inject:1 as OSGi bundle under EPL 2.0 or GPL2 w/ CPE JAXB Runtime under Eclipse Distribution License - v 1.0 JAXB2 Basics - Runtime under BSD-Style License JCL 1.2 implemented over SLF4J under Apache License, Version 2.0 - jersey-core-client under EPL 2.0 or GPL2 w/ CPE or EDL 1.0 or BSD 2-Clause or Apache License, 2.0 or Public Domain or Modified BSD or jQuery license or MIT license or W3C license - jersey-core-common under EPL 2.0 or The GNU General Public License (GPL), Version 2, With Classpath Exception or Apache License, 2.0 or Public Domain - jersey-inject-hk2 under EPL 2.0 or GPL2 w/ CPE or EDL 1.0 or BSD 2-Clause or Apache License, 2.0 or Public Domain or Modified BSD or jQuery license or MIT license or W3C license - jersey-media-multipart under EPL 2.0 or GPL2 w/ CPE or EDL 1.0 or BSD 2-Clause or Apache License, 2.0 or Public Domain or Modified BSD or jQuery license or MIT license or W3C license - MIME streaming extension under Eclipse Distribution License - v 1.0 - OSGi resource locator under EPL 2.0 or GPL2 w/ CPE Posten signering - API JAXB Classes under The Apache Software License, Version 2.0 Posten signering - API Schema under The Apache Software License, Version 2.0 Posten signering - Java API Client Library under The Apache Software License, Version 2.0 - ServiceLocator Default Implementation under EPL 2.0 or GPL2 w/ CPE SLF4J API Module under MIT License - Spring AOP under Apache License, Version 2.0 - Spring Beans under Apache License, Version 2.0 - Spring Commons Logging Bridge under Apache License, Version 2.0 - Spring Context under Apache License, Version 2.0 - Spring Core under Apache License, Version 2.0 - Spring Expression Language (SpEL) under Apache License, Version 2.0 - Spring Object/XML Marshalling under Apache License, Version 2.0 - spring-xml under Apache License, Version 2.0 TXW2 Runtime under Eclipse Distribution License - v 1.0 diff --git a/pom.xml b/pom.xml index cfc2a1ed..b0c9e45d 100644 --- a/pom.xml +++ b/pom.xml @@ -147,7 +147,7 @@ nl.jqno.equalsverifier equalsverifier - 3.10 + 3.12.4 test @@ -171,7 +171,7 @@ no.digipost digg - 0.30 + 0.33 test @@ -215,7 +215,7 @@ com.github.siom79.japicmp japicmp-maven-plugin - 0.15.7 + 0.17.1 @@ -278,7 +278,7 @@ maven-deploy-plugin - 3.0.0-M2 + 3.0.0 maven-clean-plugin @@ -286,23 +286,23 @@ maven-dependency-plugin - 3.3.0 + 3.5.0 maven-install-plugin - 3.0.0-M1 + 3.1.0 maven-resources-plugin - 3.2.0 + 3.3.0 maven-javadoc-plugin - 3.4.0 + 3.4.1 maven-jar-plugin - 3.2.2 + 3.3.0 maven-enforcer-plugin @@ -375,25 +375,6 @@ - - - jdk11+ - - [11,) - - - - true - - - - scm:git:git@github.com:digipost/signature-api-client-java.git scm:git:git@github.com:digipost/signature-api-client-java.git From d56f8ec5ffd31128c935579c62c66b1465006f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20St=C3=B8a?= Date: Thu, 9 Feb 2023 10:35:01 +0100 Subject: [PATCH 06/32] allow build workflow to fetch from github maven repo --- .github/workflows/build.yml | 18 ++++++++++-------- .mvn/settings.xml | 28 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 .mvn/settings.xml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7b9b2126..13be63ef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,13 +11,15 @@ jobs: name: build java ${{ matrix.java }} steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Set up java - uses: actions/setup-java@v1 + uses: actions/setup-java@v3.6.0 with: java-version: ${{ matrix.java }} + distribution: temurin + cache: "maven" - name: Build with Maven - run: mvn -B package --no-transfer-progress --file pom.xml + run: mvn --settings .mvn/settings.xml -B package --no-transfer-progress --file pom.xml makeversion: if: github.ref != 'refs/heads/main' @@ -35,7 +37,7 @@ jobs: else TAG=${GITHUB_REF#refs/heads/}-SNAPSHOT fi - echo ::set-output name=version::${TAG//\//-} + echo "version=${TAG//\//-}" >> $GITHUB_OUTPUT deploy_snapshot: if: startsWith(github.ref, 'refs/heads/') @@ -44,8 +46,8 @@ jobs: name: Deploy snapshot steps: - - uses: actions/checkout@v1 - - uses: digipost/action-maven-publish@1.1.0 + - uses: actions/checkout@v3 + - uses: digipost/action-maven-publish@1.2.0 with: sonatype_secrets: ${{ secrets.sonatype_secrets }} release_version: ${{ needs.makeversion.outputs.version }} @@ -58,9 +60,9 @@ jobs: name: Release to Sonatype steps: - name: Check out Git repository - uses: actions/checkout@v1 + uses: actions/checkout@v3 - name: Release to Central Repository - uses: digipost/action-maven-publish@1.1.0 + uses: digipost/action-maven-publish@1.2.0 with: sonatype_secrets: ${{ secrets.sonatype_secrets }} release_version: ${{ needs.makeversion.outputs.version }} diff --git a/.mvn/settings.xml b/.mvn/settings.xml new file mode 100644 index 00000000..cf700ad6 --- /dev/null +++ b/.mvn/settings.xml @@ -0,0 +1,28 @@ + + + + + sonatype_snapshots + + + oss.sonatype.org-snapshot + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + + + + + sonatype_snapshots + + + From 03084f07f9ef0500b9bab46ef564b790e04a5c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20St=C3=B8a?= Date: Thu, 9 Feb 2023 15:40:29 +0100 Subject: [PATCH 07/32] use jaxb-runtime 2.3.6 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b0c9e45d..45c4e7c1 100644 --- a/pom.xml +++ b/pom.xml @@ -90,7 +90,7 @@ org.glassfish.jaxb jaxb-runtime - 2.3.5 + 2.3.6 runtime From 807fe357ae93a477a6d8433a56404f805178de46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20St=C3=B8a?= Date: Fri, 10 Feb 2023 11:59:26 +0100 Subject: [PATCH 08/32] use UriBuilder to build uri's --- .../asice/signature/CreateSignature.java | 11 +++++++- .../client/core/internal/ClientHelper.java | 27 ++++++++++--------- .../client/core/internal/http/UriHelper.java | 22 --------------- .../client/direct/StatusReference.java | 4 +-- .../core/internal/http/UriHelperTest.java | 25 ----------------- 5 files changed, 26 insertions(+), 63 deletions(-) delete mode 100644 src/main/java/no/digipost/signature/client/core/internal/http/UriHelper.java delete mode 100644 src/test/java/no/digipost/signature/client/core/internal/http/UriHelperTest.java diff --git a/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java b/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java index 7c4cbc4e..852f447e 100644 --- a/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java +++ b/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java @@ -17,7 +17,16 @@ import javax.xml.crypto.NodeSetData; import javax.xml.crypto.URIDereferencer; import javax.xml.crypto.dom.DOMStructure; -import javax.xml.crypto.dsig.*; +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.SignatureMethod; +import javax.xml.crypto.dsig.SignedInfo; +import javax.xml.crypto.dsig.Transform; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.crypto.dsig.XMLSignatureException; +import javax.xml.crypto.dsig.XMLSignatureFactory; import javax.xml.crypto.dsig.dom.DOMSignContext; import javax.xml.crypto.dsig.keyinfo.KeyInfo; import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; diff --git a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java index b1a7d195..d3bab613 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java @@ -1,5 +1,9 @@ package no.digipost.signature.client.core.internal; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response.StatusType; +import jakarta.ws.rs.core.UriBuilder; import no.digipost.signature.api.xml.XMLDirectSignatureJobRequest; import no.digipost.signature.api.xml.XMLDirectSignatureJobResponse; import no.digipost.signature.api.xml.XMLDirectSignatureJobStatusResponse; @@ -25,17 +29,12 @@ import no.digipost.signature.client.core.exceptions.UnexpectedResponseException; import no.digipost.signature.client.core.internal.http.ResponseStatus; import no.digipost.signature.client.core.internal.http.SignatureHttpClient; -import no.digipost.signature.client.core.internal.http.UriHelper; import no.digipost.signature.client.core.internal.xml.Marshalling; import no.digipost.signature.client.direct.WithSignerUrl; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response.StatusType; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -54,17 +53,18 @@ import java.util.Random; import java.util.function.Supplier; -import static jakarta.ws.rs.core.HttpHeaders.*; -import static java.time.format.DateTimeFormatter.ISO_DATE_TIME; +import static jakarta.ws.rs.core.HttpHeaders.ACCEPT; +import static jakarta.ws.rs.core.HttpHeaders.CONTENT_LENGTH; import static jakarta.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE; import static jakarta.ws.rs.core.MediaType.APPLICATION_XML_TYPE; import static jakarta.ws.rs.core.Response.Status.CONFLICT; import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; +import static jakarta.ws.rs.core.Response.Status.Family.SUCCESSFUL; import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; import static jakarta.ws.rs.core.Response.Status.NO_CONTENT; import static jakarta.ws.rs.core.Response.Status.OK; import static jakarta.ws.rs.core.Response.Status.TOO_MANY_REQUESTS; -import static jakarta.ws.rs.core.Response.Status.Family.SUCCESSFUL; +import static java.time.format.DateTimeFormatter.ISO_DATE_TIME; import static no.digipost.signature.client.core.internal.ActualSender.getActualSender; import static no.digipost.signature.client.core.internal.ErrorCodes.BROKER_NOT_AUTHORIZED; import static no.digipost.signature.client.core.internal.ErrorCodes.SIGNING_CEREMONY_NOT_COMPLETED; @@ -100,6 +100,7 @@ public XMLPortalSignatureJobResponse sendPortalSignatureJobRequest(XMLPortalSign return multipartSignatureJobRequest(signatureJobRequest, documentBundle, actualSender, XMLPortalSignatureJobResponse.class); } + // TODO: Add TARGET, not only use DIRECT private RESPONSE multipartSignatureJobRequest(REQUEST signatureJobRequest, DocumentBundle documentBundle, Sender actualSender, Class responseClass) { return call(() -> { try { @@ -124,8 +125,8 @@ private RESPONSE multipartSignatureJobRequest(REQUEST signat byteArrays.add(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8)); var request = HttpRequest.newBuilder() - .uri(httpClient.signatureServiceRoot().resolve(DIRECT.path(actualSender))) - .header("Content-Type", "multipart/form-data;boundary=" + boundary) + .uri(UriBuilder.fromUri(httpClient.signatureServiceRoot()).path(DIRECT.path(actualSender)).build()) + .header("Content-Type", "multipart/mixed;boundary=" + boundary) .header(ACCEPT, APPLICATION_XML_TYPE.getType()) .POST(HttpRequest.BodyPublishers.ofByteArrays(byteArrays)) .build(); @@ -162,7 +163,7 @@ public XMLDirectSignerResponse requestNewRedirectUrl(WithSignerUrl url) { public XMLDirectSignatureJobStatusResponse sendSignatureJobStatusRequest(final URI statusUrl) { return call(() -> { var request = HttpRequest.newBuilder() - .uri(httpClient.signatureServiceRoot().resolve(statusUrl)) + .uri(statusUrl) .header(ACCEPT, APPLICATION_XML_TYPE.getType()) .GET() .build(); @@ -243,7 +244,7 @@ private JobStatusResponse getStatusChange(final return call(() -> { Sender actualSender = getActualSender(sender, globalSender); var request = HttpRequest.newBuilder() - .uri(UriHelper.addQuery(httpClient.signatureServiceRoot().resolve(target.path(actualSender)), POLLING_QUEUE_QUERY_PARAMETER + "=" + actualSender.getPollingQueue().value)) + .uri(UriBuilder.fromUri(httpClient.signatureServiceRoot()).path(target.path(actualSender)).queryParam(POLLING_QUEUE_QUERY_PARAMETER, actualSender.getPollingQueue().value).build()) .header(ACCEPT, APPLICATION_XML_TYPE.getType()) .GET() .build(); @@ -382,5 +383,5 @@ private static XMLError extractError(HttpResponse response) { } return error; } - + } diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/UriHelper.java b/src/main/java/no/digipost/signature/client/core/internal/http/UriHelper.java deleted file mode 100644 index 906446c7..00000000 --- a/src/main/java/no/digipost/signature/client/core/internal/http/UriHelper.java +++ /dev/null @@ -1,22 +0,0 @@ -package no.digipost.signature.client.core.internal.http; - -import java.net.URI; -import java.net.URISyntaxException; - -public class UriHelper { - - public static URI addQuery(URI uri, String query) { - String newQuery = uri.getQuery(); - if (newQuery == null) { - newQuery = query; - } else { - newQuery += "&" + query; - } - - try { - return new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), newQuery, uri.getFragment()); - } catch (URISyntaxException e) { - throw new RuntimeException("Could not append query [" + query + "] to uri [" + uri + "]", e); - } - } -} diff --git a/src/main/java/no/digipost/signature/client/direct/StatusReference.java b/src/main/java/no/digipost/signature/client/direct/StatusReference.java index eaac236d..dc437a62 100644 --- a/src/main/java/no/digipost/signature/client/direct/StatusReference.java +++ b/src/main/java/no/digipost/signature/client/direct/StatusReference.java @@ -1,6 +1,6 @@ package no.digipost.signature.client.direct; -import no.digipost.signature.client.core.internal.http.UriHelper; +import jakarta.ws.rs.core.UriBuilder; import java.net.URI; import java.util.Objects; @@ -54,7 +54,7 @@ private StatusReference(URI statusUrl, String statusQueryToken) { } public URI getStatusUrl() { - return UriHelper.addQuery(statusUrl,STATUS_QUERY_TOKEN_PARAM_NAME + "=" + statusQueryToken); + return UriBuilder.fromUri(statusUrl).queryParam(STATUS_QUERY_TOKEN_PARAM_NAME, statusQueryToken).build(); } public static abstract class StatusUrlContruction { diff --git a/src/test/java/no/digipost/signature/client/core/internal/http/UriHelperTest.java b/src/test/java/no/digipost/signature/client/core/internal/http/UriHelperTest.java deleted file mode 100644 index e3a36b63..00000000 --- a/src/test/java/no/digipost/signature/client/core/internal/http/UriHelperTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package no.digipost.signature.client.core.internal.http; - -import org.junit.jupiter.api.Test; - -import java.net.URI; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -public class UriHelperTest { - - @Test - void appendsQueryParam() { - var uri = URI.create("https://test.example.com"); - var withQuery = UriHelper.addQuery(uri, "key=value"); - assertThat(withQuery, is(URI.create("https://test.example.com?key=value"))); - } - - @Test - void appendsQueryOnUriWithQuery() { - var uri = URI.create("https://test.example.com?key1=value1"); - var withQuery = UriHelper.addQuery(uri, "key2=value2"); - assertThat(withQuery, is(URI.create("https://test.example.com?key1=value1&key2=value2"))); - } -} From 81265d81da014ee904901d0bc8d4abdfd01417ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20St=C3=B8a?= Date: Mon, 13 Feb 2023 16:06:36 +0100 Subject: [PATCH 09/32] remove use of jakarta rs and use apache http client instead of java http client --- NOTICE | 4 +- pom.xml | 31 +- .../signature/client/ClientConfiguration.java | 66 ++-- .../exceptions/CantQueryStatusException.java | 6 +- .../JobCannotBeCancelledException.java | 9 +- .../UnexpectedResponseException.java | 36 +- .../core/internal/ClientExceptionMapper.java | 17 +- .../client/core/internal/ClientHelper.java | 329 +++++++++--------- .../internal/http/AddRequestHeaderFilter.java | 27 -- .../http/HttpIntegrationConfiguration.java | 6 +- .../core/internal/http/ResponseStatus.java | 182 ++-------- .../http/SignatureApiTrustStrategy.java | 2 +- .../internal/http/SignatureHttpClient.java | 6 +- .../http/SignatureHttpClientFactory.java | 16 +- .../client/core/internal/http/StatusCode.java | 52 +++ .../core/internal/http/StatusCodeFamily.java | 22 ++ .../signature/client/direct/DirectClient.java | 9 +- .../client/direct/StatusReference.java | 9 +- .../signature/client/portal/PortalClient.java | 9 +- .../internal/http/ResponseStatusTest.java | 28 +- .../http/SignatureHttpClientFactoryTest.java | 11 +- 21 files changed, 370 insertions(+), 507 deletions(-) delete mode 100644 src/main/java/no/digipost/signature/client/core/internal/http/AddRequestHeaderFilter.java create mode 100644 src/main/java/no/digipost/signature/client/core/internal/http/StatusCode.java create mode 100644 src/main/java/no/digipost/signature/client/core/internal/http/StatusCodeFamily.java diff --git a/NOTICE b/NOTICE index 9c6c81fe..4c242e6c 100644 --- a/NOTICE +++ b/NOTICE @@ -11,13 +11,13 @@ This software includes third party software subject to the following licenses: Apache Commons Codec under Apache License, Version 2.0 Apache Commons Lang under Apache License, Version 2.0 Apache HttpClient under Apache License, Version 2.0 + Apache HttpComponents Core HTTP/1.1 under Apache License, Version 2.0 + Apache HttpComponents Core HTTP/2 under Apache License, Version 2.0 Apache HttpCore under Apache License, Version 2.0 Digipost Certificate Validator under The Apache Software License, Version 2.0 istack common utility code runtime under Eclipse Distribution License - v 1.0 Jakarta Activation under EDL 1.0 Jakarta Activation API jar under EDL 1.0 - Jakarta Annotations API under EPL 2.0 or GPL2 w/ CPE - Jakarta RESTful WS API under EPL-2.0 or GPL-2.0-with-classpath-exception Jakarta XML Binding API under Eclipse Distribution License - v 1.0 JAXB Runtime under Eclipse Distribution License - v 1.0 JAXB2 Basics - Runtime under BSD-Style License diff --git a/pom.xml b/pom.xml index 45c4e7c1..cf6eac5f 100644 --- a/pom.xml +++ b/pom.xml @@ -56,32 +56,18 @@ + - org.apache.httpcomponents - httpclient - 4.5.13 - - - commons-logging - commons-logging - - - - - org.apache.httpcomponents - httpcore - 4.4.15 - - - jakarta.annotation - jakarta.annotation-api - 2.1.1 + org.apache.httpcomponents.client5 + httpclient5 + 5.1.3 - jakarta.ws.rs - jakarta.ws.rs-api - 3.1.0 + org.apache.httpcomponents.core5 + httpcore5 + 5.1.3 + jakarta.xml.bind jakarta.xml.bind-api @@ -93,6 +79,7 @@ 2.3.6 runtime + commons-io commons-io diff --git a/src/main/java/no/digipost/signature/client/ClientConfiguration.java b/src/main/java/no/digipost/signature/client/ClientConfiguration.java index 684a66b8..b76da294 100644 --- a/src/main/java/no/digipost/signature/client/ClientConfiguration.java +++ b/src/main/java/no/digipost/signature/client/ClientConfiguration.java @@ -1,6 +1,5 @@ package no.digipost.signature.client; -import jakarta.ws.rs.core.HttpHeaders; import no.digipost.signature.client.asice.ASiCEConfiguration; import no.digipost.signature.client.asice.DocumentBundleProcessor; import no.digipost.signature.client.asice.DumpDocumentBundleToDisk; @@ -15,15 +14,23 @@ import no.digipost.signature.client.security.KeyStoreConfig; import no.digipost.signature.client.security.OrganizationNumberValidation; import org.apache.commons.lang3.StringUtils; -import org.apache.http.ssl.SSLContexts; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.SocketConfig; +import org.apache.hc.core5.ssl.SSLContexts; +import org.apache.hc.core5.util.Timeout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.SSLContext; import java.io.InputStream; -import java.net.ProxySelector; import java.net.URI; -import java.net.http.HttpClient; import java.nio.file.Path; import java.security.KeyManagementException; import java.security.KeyStoreException; @@ -77,7 +84,7 @@ public final class ClientConfiguration implements ProvidesCertificateResourcePat private final KeyStoreConfig keyStoreConfig; - private final HttpClient httpClient; + private final org.apache.hc.client5.http.classic.HttpClient httpClient; private final Duration socketTimeout; private final Optional sender; private final URI signatureServiceRoot; @@ -86,7 +93,7 @@ public final class ClientConfiguration implements ProvidesCertificateResourcePat private final Clock clock; private ClientConfiguration( - KeyStoreConfig keyStoreConfig, HttpClient httpClient, Duration socketTimeout, Optional sender, + KeyStoreConfig keyStoreConfig, org.apache.hc.client5.http.classic.HttpClient httpClient, Duration socketTimeout, Optional sender, URI serviceRoot, Iterable certificatePaths, Iterable documentBundleProcessors, Clock clock) { this.keyStoreConfig = keyStoreConfig; @@ -130,11 +137,6 @@ public HttpClient httpClient() { return httpClient; } - @Override - public Duration socketTimeout() { - return socketTimeout; - } - /** * Build a new {@link ClientConfiguration}. */ @@ -149,10 +151,12 @@ public Iterable getCertificatePaths() { public static class Builder { - private final HttpClient.Builder httpClientBuilder; + private final HttpClientBuilder httpClientBuilder; private final KeyStoreConfig keyStoreConfig; private Duration socketTimeout = DEFAULT_SOCKET_TIMEOUT; + private Duration connectTimeout = DEFAULT_CONNECT_TIMEOUT; + private Optional customUserAgentPart = Optional.empty(); private URI serviceRoot = ServiceUri.PRODUCTION.uri; private Optional globalSender = Optional.empty(); @@ -160,14 +164,13 @@ public static class Builder { private CertificateChainValidation serverCertificateTrustStrategy = new OrganizationNumberValidation("984661185"); // Posten Norge AS organization number private List documentBundleProcessors = new ArrayList<>(); private Clock clock = Clock.systemDefaultZone(); - private Optional proxy = Optional.empty(); + private Optional proxy = Optional.empty(); + private Optional> httpClientCustomizer = Optional.empty(); private Builder(KeyStoreConfig keyStoreConfig) { this.keyStoreConfig = keyStoreConfig; - this.httpClientBuilder = HttpClient.newBuilder() - .connectTimeout(DEFAULT_CONNECT_TIMEOUT) - .followRedirects(HttpClient.Redirect.ALWAYS); + this.httpClientBuilder = HttpClientBuilder.create(); } /** @@ -199,14 +202,14 @@ public Builder socketTimeout(Duration timeout) { * {@link ClientConfiguration#DEFAULT_CONNECT_TIMEOUT default connect timeout value}. */ public Builder connectTimeout(Duration timeout) { - this.httpClientBuilder.connectTimeout(timeout); + this.connectTimeout = timeout; return this; } /** * Set proxy to be used by {@link HttpClient}. */ - public void proxy(ProxySelector proxy) { + public void proxy(HttpHost proxy) { this.proxy = Optional.of(proxy); } @@ -314,10 +317,10 @@ public Builder addDocumentBundleProcessor(DocumentBundleProcessor processor) { * If you still need to use this method, consider requesting first-class support for your requirement * on the library's web site on GitHub. * - * @param customizer The operations to do on the {@link HttpClient.Builder}. + * @param customizer The operations to do on the {@link HttpClientBuilder}. */ - public Builder customizeHttpClient(Consumer customizer) { - customizer.accept(this.httpClientBuilder); + public Builder customizeHttpClient(Consumer customizer) { + this.httpClientCustomizer = Optional.of(customizer); return this; } @@ -363,10 +366,23 @@ public Builder usingClock(Clock clock) { public ClientConfiguration build() { // TODO: Add possibility to add logger for http client - var httpClient = httpClientBuilder - .sslContext(sslContext()); - proxy.ifPresent(httpClient::proxy); - return new ClientConfiguration(keyStoreConfig, httpClient.build(), socketTimeout, globalSender, serviceRoot, certificatePaths, documentBundleProcessors, clock); + + var socketConfig = SocketConfig.custom().setSoTimeout(Timeout.ofMilliseconds(socketTimeout.toMillis())).build(); + var poolingHttpClientConnectionManager = PoolingHttpClientConnectionManagerBuilder.create() + .setDefaultSocketConfig(socketConfig) + .setSSLSocketFactory(SSLConnectionSocketFactoryBuilder.create() + .setSslContext(sslContext()) + .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) + .build()); + var requestConfigBuilder = RequestConfig.custom().setConnectionRequestTimeout(Timeout.ofMilliseconds(connectTimeout.toMillis())); + httpClientBuilder + .setConnectionManager(poolingHttpClientConnectionManager.build()) + .setDefaultRequestConfig(requestConfigBuilder.build()) + .setUserAgent(createUserAgentString()); + proxy.ifPresent(httpClientBuilder::setProxy); + httpClientCustomizer.ifPresent(customizer -> customizer.accept(httpClientBuilder)); + + return new ClientConfiguration(keyStoreConfig, httpClientBuilder.build(), socketTimeout, globalSender, serviceRoot, certificatePaths, documentBundleProcessors, clock); } String createUserAgentString() { diff --git a/src/main/java/no/digipost/signature/client/core/exceptions/CantQueryStatusException.java b/src/main/java/no/digipost/signature/client/core/exceptions/CantQueryStatusException.java index e0fb8de0..0d85b01e 100644 --- a/src/main/java/no/digipost/signature/client/core/exceptions/CantQueryStatusException.java +++ b/src/main/java/no/digipost/signature/client/core/exceptions/CantQueryStatusException.java @@ -1,13 +1,13 @@ package no.digipost.signature.client.core.exceptions; -import jakarta.ws.rs.core.Response.StatusType; +import no.digipost.signature.client.core.internal.http.StatusCode; public class CantQueryStatusException extends SignatureException { - public CantQueryStatusException(StatusType status, String errorMessageFromServer) { + public CantQueryStatusException(StatusCode status, String errorMessageFromServer) { super("The service refused to process the status request. This happens when the job has not been completed " + "(i.e. the signer haven't signed or rejected). Please wait until the signer have been redirected to " + "one of the exit URLs provided in the initial request before querying the job's status. The server response was " + - status.getStatusCode() + " " + status.getReasonPhrase() + " '" + errorMessageFromServer + "'"); + status.value() + " '" + errorMessageFromServer + "'"); } } diff --git a/src/main/java/no/digipost/signature/client/core/exceptions/JobCannotBeCancelledException.java b/src/main/java/no/digipost/signature/client/core/exceptions/JobCannotBeCancelledException.java index 64e55c4f..891dc0c9 100644 --- a/src/main/java/no/digipost/signature/client/core/exceptions/JobCannotBeCancelledException.java +++ b/src/main/java/no/digipost/signature/client/core/exceptions/JobCannotBeCancelledException.java @@ -1,20 +1,19 @@ package no.digipost.signature.client.core.exceptions; import no.digipost.signature.api.xml.XMLError; - -import jakarta.ws.rs.core.Response.StatusType; +import no.digipost.signature.client.core.internal.http.StatusCode; public class JobCannotBeCancelledException extends SignatureException { - public JobCannotBeCancelledException(StatusType status, XMLError errorEntity) { + public JobCannotBeCancelledException(StatusCode status, XMLError errorEntity) { this(status, errorEntity.getErrorCode(), errorEntity.getErrorMessage()); } - public JobCannotBeCancelledException(StatusType status, String errorCode, String errorMessageFromServer) { + public JobCannotBeCancelledException(StatusCode status, String errorCode, String errorMessageFromServer) { super("The service refused to process the cancellation. This happens when the job has been completed " + "(i.e. all signers have signed or rejected, the job has expired, etc.) since receiving last update. " + "Please ask the service for status changes to get the latest changes. The server response was " + - status.getStatusCode() + " " + status.getReasonPhrase() + " '" + errorCode + ": " + errorMessageFromServer + "'"); + status.value() + " '" + errorCode + ": " + errorMessageFromServer + "'"); } } diff --git a/src/main/java/no/digipost/signature/client/core/exceptions/UnexpectedResponseException.java b/src/main/java/no/digipost/signature/client/core/exceptions/UnexpectedResponseException.java index bfe91c09..16db824d 100644 --- a/src/main/java/no/digipost/signature/client/core/exceptions/UnexpectedResponseException.java +++ b/src/main/java/no/digipost/signature/client/core/exceptions/UnexpectedResponseException.java @@ -1,49 +1,35 @@ package no.digipost.signature.client.core.exceptions; import no.digipost.signature.api.xml.XMLError; - -import jakarta.ws.rs.core.Response.Status; -import jakarta.ws.rs.core.Response.StatusType; +import no.digipost.signature.client.core.internal.http.StatusCode; import java.util.Objects; public class UnexpectedResponseException extends SignatureException { private final XMLError error; - private final StatusType actualStatus; + private final int statusCode; - public UnexpectedResponseException(StatusType actual) { + public UnexpectedResponseException(StatusCode actual) { this(null, actual); } - public UnexpectedResponseException(Object errorEntity, StatusType actual, StatusType ... expected) { + public UnexpectedResponseException(Object errorEntity, StatusCode actual, StatusCode ... expected) { this(errorEntity, null, actual, expected); } - public UnexpectedResponseException(Object errorEntity, Throwable cause, StatusType actual, StatusType ... expected) { + public UnexpectedResponseException(Object errorEntity, Throwable cause, StatusCode actual, StatusCode ... expected) { super("Expected " + prettyprintExpectedStatuses(expected) + - ", but got " + actual.getStatusCode() + " " + actual.getReasonPhrase() + + ", but got " + actual.value() + (errorEntity != null ? " [" + errorEntity + "]" : "") + (cause != null ? " - " + cause.getClass().getSimpleName() + ": '" + cause.getMessage() + "'.": ""), cause); this.error = errorEntity instanceof XMLError ? (XMLError) errorEntity : null; - this.actualStatus = actual; - } - - public StatusType getActualStatus() { - return actualStatus; - } - - public boolean is(Status.Family family) { - return actualStatus != null && actualStatus.getFamily() == family; + this.statusCode = actual.value(); } public boolean isStatusCode(int statusCode) { - return actualStatus != null && actualStatus.getStatusCode() == statusCode; - } - - public boolean isStatusCodeOf(StatusType status) { - return isStatusCode(status.getStatusCode()); + return this.statusCode == statusCode; } public String getErrorCode() { @@ -62,7 +48,7 @@ public boolean isErrorCode(String errorCode) { return error != null & Objects.equals(error.getErrorCode(), errorCode); } - private static String prettyprintExpectedStatuses(StatusType ... statuses) { + private static String prettyprintExpectedStatuses(StatusCode ... statuses) { if (statuses == null || statuses.length == 0) { return "status not specified"; } @@ -73,8 +59,8 @@ private static String prettyprintExpectedStatuses(StatusType ... statuses) { return message + "]"; } - private static String prettyprintSingleStatus(StatusType status) { - return status.getStatusCode() + " " + status.getReasonPhrase(); + private static String prettyprintSingleStatus(StatusCode status) { + return status.value() +""; } } diff --git a/src/main/java/no/digipost/signature/client/core/internal/ClientExceptionMapper.java b/src/main/java/no/digipost/signature/client/core/internal/ClientExceptionMapper.java index 86bd5054..d852d90e 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/ClientExceptionMapper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/ClientExceptionMapper.java @@ -1,11 +1,8 @@ package no.digipost.signature.client.core.internal; -import no.digipost.signature.client.core.exceptions.ConfigurationException; import no.digipost.signature.client.core.exceptions.SignatureException; -import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; -import jakarta.ws.rs.ProcessingException; import java.util.function.Supplier; class ClientExceptionMapper { @@ -20,23 +17,13 @@ void doWithMappedClientException(Runnable action) { T doWithMappedClientException(Supplier produceResult) { try { return produceResult.get(); - } catch (ProcessingException e) { + } catch (RuntimeException e) { throw map(e); } } - private RuntimeException map(ProcessingException e) { - if (e.getCause() instanceof SSLException) { - String sslExceptionMessage = e.getCause().getMessage(); - if (sslExceptionMessage != null && sslExceptionMessage.contains("protocol_version")) { - return new ConfigurationException( - "Invalid TLS protocol version. This will typically happen if you're running on an older Java version, which doesn't support TLS 1.2. " + - "Java 7 needs to be explicitly configured to support TLS 1.2. See 'JSSE tuning parameters' at " + - "https://blogs.oracle.com/java-platform-group/entry/diagnosing_tls_ssl_and_https.", e); - } - } - + private RuntimeException map(RuntimeException e) { if (e.getCause() instanceof SSLHandshakeException) { return new SignatureException( "Unable to perform SSL handshake with remote server. Some possible causes (could be others, see underlying error): \n" + diff --git a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java index d3bab613..ae81b1df 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java @@ -1,9 +1,5 @@ package no.digipost.signature.client.core.internal; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response.StatusType; -import jakarta.ws.rs.core.UriBuilder; import no.digipost.signature.api.xml.XMLDirectSignatureJobRequest; import no.digipost.signature.api.xml.XMLDirectSignatureJobResponse; import no.digipost.signature.api.xml.XMLDirectSignatureJobStatusResponse; @@ -29,47 +25,52 @@ import no.digipost.signature.client.core.exceptions.UnexpectedResponseException; import no.digipost.signature.client.core.internal.http.ResponseStatus; import no.digipost.signature.client.core.internal.http.SignatureHttpClient; +import no.digipost.signature.client.core.internal.http.StatusCode; +import no.digipost.signature.client.core.internal.http.StatusCodeFamily; import no.digipost.signature.client.core.internal.xml.Marshalling; import no.digipost.signature.client.direct.WithSignerUrl; import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.entity.mime.ByteArrayBody; +import org.apache.hc.client5.http.entity.mime.InputStreamBody; +import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; +import org.apache.hc.client5.http.entity.mime.MultipartPartBuilder; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; +import org.apache.hc.core5.net.URIBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.UncheckedIOException; -import java.math.BigInteger; import java.net.URI; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodyHandlers; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.ZonedDateTime; -import java.util.ArrayList; import java.util.Arrays; import java.util.Optional; -import java.util.Random; import java.util.function.Supplier; -import static jakarta.ws.rs.core.HttpHeaders.ACCEPT; -import static jakarta.ws.rs.core.HttpHeaders.CONTENT_LENGTH; -import static jakarta.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE; -import static jakarta.ws.rs.core.MediaType.APPLICATION_XML_TYPE; -import static jakarta.ws.rs.core.Response.Status.CONFLICT; -import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; -import static jakarta.ws.rs.core.Response.Status.Family.SUCCESSFUL; -import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; -import static jakarta.ws.rs.core.Response.Status.NO_CONTENT; -import static jakarta.ws.rs.core.Response.Status.OK; -import static jakarta.ws.rs.core.Response.Status.TOO_MANY_REQUESTS; import static java.time.format.DateTimeFormatter.ISO_DATE_TIME; import static no.digipost.signature.client.core.internal.ActualSender.getActualSender; import static no.digipost.signature.client.core.internal.ErrorCodes.BROKER_NOT_AUTHORIZED; import static no.digipost.signature.client.core.internal.ErrorCodes.SIGNING_CEREMONY_NOT_COMPLETED; import static no.digipost.signature.client.core.internal.Target.DIRECT; import static no.digipost.signature.client.core.internal.Target.PORTAL; +import static org.apache.hc.core5.http.ContentType.APPLICATION_OCTET_STREAM; +import static org.apache.hc.core5.http.ContentType.APPLICATION_XML; +import static org.apache.hc.core5.http.ContentType.MULTIPART_MIXED; +import static org.apache.hc.core5.http.HttpHeaders.ACCEPT; +import static org.apache.hc.core5.http.HttpHeaders.CONTENT_TYPE; public class ClientHelper { @@ -91,129 +92,106 @@ public ClientHelper(SignatureHttpClient httpClient, Optional globalSende public XMLDirectSignatureJobResponse sendSignatureJobRequest(XMLDirectSignatureJobRequest signatureJobRequest, DocumentBundle documentBundle, Optional sender) { final Sender actualSender = getActualSender(sender, globalSender); - return multipartSignatureJobRequest(signatureJobRequest, documentBundle, actualSender, XMLDirectSignatureJobResponse.class); + return multipartSignatureJobRequest(signatureJobRequest, documentBundle, actualSender, DIRECT, XMLDirectSignatureJobResponse.class); } public XMLPortalSignatureJobResponse sendPortalSignatureJobRequest(XMLPortalSignatureJobRequest signatureJobRequest, DocumentBundle documentBundle, Optional sender) { final Sender actualSender = getActualSender(sender, globalSender); - return multipartSignatureJobRequest(signatureJobRequest, documentBundle, actualSender, XMLPortalSignatureJobResponse.class); + return multipartSignatureJobRequest(signatureJobRequest, documentBundle, actualSender, PORTAL, XMLPortalSignatureJobResponse.class); } - // TODO: Add TARGET, not only use DIRECT - private RESPONSE multipartSignatureJobRequest(REQUEST signatureJobRequest, DocumentBundle documentBundle, Sender actualSender, Class responseClass) { + private RESPONSE multipartSignatureJobRequest(REQUEST signatureJobRequest, DocumentBundle documentBundle, Sender actualSender, Target target, Class responseClass) { return call(() -> { try { - String boundary = new BigInteger(256, new Random()).toString(); - var byteArrays = new ArrayList(); - byte[] separator = ("--" + boundary + "\r\nContent-Disposition: form-data; name=") - .getBytes(StandardCharsets.UTF_8); - - byteArrays.add(separator); + var multipartEntityBuilder = MultipartEntityBuilder.create(); + multipartEntityBuilder.setContentType(MULTIPART_MIXED); try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { Marshalling.marshal(signatureJobRequest, os); - byteArrays.add(("\"\r\nContent-Type: " + APPLICATION_XML_TYPE.getType() + "\r\n\r\n").getBytes(StandardCharsets.UTF_8)); - byteArrays.add(os.toByteArray()); + multipartEntityBuilder.addPart(MultipartPartBuilder.create() + .setBody(new ByteArrayBody(os.toByteArray(), APPLICATION_XML, "")) + .addHeader(CONTENT_TYPE, APPLICATION_XML.getMimeType()) + .build()); } + multipartEntityBuilder.addPart(MultipartPartBuilder.create() + .setBody(new InputStreamBody(documentBundle.getInputStream(), APPLICATION_OCTET_STREAM, "")) + .addHeader(CONTENT_TYPE, APPLICATION_OCTET_STREAM.getMimeType()).build()); - byteArrays.add(separator); - - byteArrays.add(("\"\r\nContent-Type: " + APPLICATION_OCTET_STREAM_TYPE.getType() + "\r\n\r\n").getBytes(StandardCharsets.UTF_8)); - byteArrays.add(documentBundle.getInputStream().readAllBytes()); + try (HttpEntity multiPart = multipartEntityBuilder.build()) { + var request = ClassicRequestBuilder + .post(new URIBuilder(httpClient.signatureServiceRoot()).appendPath(target.path(actualSender)).build()) + .addHeader(ACCEPT, APPLICATION_XML.getMimeType()) + .build(); - byteArrays.add(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8)); + request.setEntity(multiPart); - var request = HttpRequest.newBuilder() - .uri(UriBuilder.fromUri(httpClient.signatureServiceRoot()).path(DIRECT.path(actualSender)).build()) - .header("Content-Type", "multipart/mixed;boundary=" + boundary) - .header(ACCEPT, APPLICATION_XML_TYPE.getType()) - .POST(HttpRequest.BodyPublishers.ofByteArrays(byteArrays)) - .build(); - - var response = httpClient.httpClient().send(request, BodyHandlers.ofInputStream()); - return parseResponse(response, responseClass); + return httpClient.httpClient().execute(request, response -> parseResponse(response, responseClass)); + } } catch (IOException e) { throw new UncheckedIOException(e); - } catch (InterruptedException e) { + } catch (URISyntaxException e) { throw new RuntimeException(e); } }); } public XMLDirectSignerResponse requestNewRedirectUrl(WithSignerUrl url) { - // TODO: Er det noe annet å bruke i stede for? try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { Marshalling.marshal(new XMLDirectSignerUpdateRequest().withRedirectUrl(new XMLEmptyElement()), os); - var request = HttpRequest.newBuilder() - .uri(url.getSignerUrl()) - .header(ACCEPT, APPLICATION_XML_TYPE.getType()) - .POST(HttpRequest.BodyPublishers.ofByteArray(os.toByteArray())) - .build(); + var request = new HttpPost(url.getSignerUrl()); + request.addHeader(ACCEPT, APPLICATION_XML.getMimeType()); - var result = httpClient.httpClient().send(request, BodyHandlers.ofInputStream()); - return parseResponse(result, XMLDirectSignerResponse.class); + return httpClient.httpClient().execute(request, response -> parseResponse(response, XMLDirectSignerResponse.class)); } catch (IOException e) { throw new UncheckedIOException(e); - } catch (InterruptedException e) { - throw new RuntimeException(e); } } public XMLDirectSignatureJobStatusResponse sendSignatureJobStatusRequest(final URI statusUrl) { return call(() -> { - var request = HttpRequest.newBuilder() - .uri(statusUrl) - .header(ACCEPT, APPLICATION_XML_TYPE.getType()) - .GET() - .build(); + var request = new HttpGet(statusUrl); + request.addHeader(ACCEPT, APPLICATION_XML.getMimeType()); + try { - var result = httpClient.httpClient().send(request, BodyHandlers.ofInputStream()); - ResponseStatus.resolve(result.statusCode()).expect(SUCCESSFUL).orThrow(status -> { - if (status == FORBIDDEN) { - XMLError error = extractError(result); - if (ErrorCodes.INVALID_STATUS_QUERY_TOKEN.sameAs(error.getErrorCode())) { - return new InvalidStatusQueryTokenException(statusUrl, error.getErrorMessage()); - } - } else if (status == NOT_FOUND) { - XMLError error = extractError(result); - if (SIGNING_CEREMONY_NOT_COMPLETED.sameAs(error.getErrorCode())) { - return new CantQueryStatusException(status, error.getErrorMessage()); + return httpClient.httpClient().execute(request, response -> { + ResponseStatus.resolve(response.getCode()).expect(StatusCodeFamily.SUCCESSFUL).orThrow(status -> { + if (status.value() == HttpStatus.SC_FORBIDDEN) { + XMLError error = extractError(response); + if (ErrorCodes.INVALID_STATUS_QUERY_TOKEN.sameAs(error.getErrorCode())) { + return new InvalidStatusQueryTokenException(statusUrl, error.getErrorMessage()); + } + } else if (status.value() == HttpStatus.SC_NOT_FOUND) { + XMLError error = extractError(response); + if (SIGNING_CEREMONY_NOT_COMPLETED.sameAs(error.getErrorCode())) { + return new CantQueryStatusException(status, error.getErrorMessage()); + } } - } - return exceptionForGeneralError(result); + return exceptionForGeneralError(response); + }); + return parseResponse(response, XMLDirectSignatureJobStatusResponse.class); }); - return parseResponse(result, XMLDirectSignatureJobStatusResponse.class); - } catch (InterruptedException e) { - throw new RuntimeException(e); } catch (IOException e) { throw new UncheckedIOException(e); } }); } - public ResponseInputStream getDataStream(URI uri, MediaType ... acceptedResponses) { + public ResponseInputStream getDataStream(URI uri, ContentType ... acceptedResponses) { return call(() -> { - var requestBuilder = HttpRequest.newBuilder() - .uri(uri) - .GET(); + var request = new HttpGet(uri); - Arrays.stream(acceptedResponses).forEach(mediaType -> { - requestBuilder.header(ACCEPT, mediaType.getType()); - }); + Arrays.stream(acceptedResponses).forEach(mediaType -> request.addHeader(ACCEPT, mediaType.getMimeType())); try { - HttpResponse response = httpClient.httpClient().send(requestBuilder.build(), BodyHandlers.ofInputStream()); - ResponseStatus.resolve(response.statusCode()).expect(SUCCESSFUL).orThrow(unexpectedStatus -> exceptionForGeneralError(response)); - return new ResponseInputStream( - response.body(), - Integer.parseInt(response.headers() - .firstValue(CONTENT_LENGTH) - .orElseThrow(() -> new RuntimeException("Expected header " + CONTENT_LENGTH + " to exist")))); + return httpClient.httpClient().execute(request, response -> { + ResponseStatus.resolve(response.getCode()).expect(StatusCodeFamily.SUCCESSFUL).orThrow(unexpectedStatus -> exceptionForGeneralError(response)); + return new ResponseInputStream( + response.getEntity().getContent(), + Math.toIntExact(response.getEntity().getContentLength())); + }); } catch (IOException e) { throw new UncheckedIOException(e); - } catch (InterruptedException e) { - throw new RuntimeException(e); } }); } @@ -221,11 +199,14 @@ public ResponseInputStream getDataStream(URI uri, MediaType ... acceptedResponse public void cancel(final Cancellable cancellable) { call(() -> { if (cancellable.getCancellationUrl() != null) { - var response = postEmptyEntity(cancellable.getCancellationUrl().getUrl()); - ResponseStatus.resolve(response.statusCode()) - .throwIf(CONFLICT, status -> new JobCannotBeCancelledException(status, extractError(response))) - .expect(SUCCESSFUL) - .orThrow(status -> exceptionForGeneralError(response)); + try(var response = postEmptyEntity(cancellable.getCancellationUrl().getUrl())) { + ResponseStatus.resolve(response.getCode()) + .throwIf(HttpStatus.SC_CONFLICT, status -> new JobCannotBeCancelledException(status, extractError(response))) + .expect(StatusCodeFamily.SUCCESSFUL) + .orThrow(status -> exceptionForGeneralError(response)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } else { throw new NotCancellableException(); } @@ -243,29 +224,32 @@ public JobStatusResponse getDirectStatusCha private JobStatusResponse getStatusChange(final Optional sender, final Target target, final Class responseClass) { return call(() -> { Sender actualSender = getActualSender(sender, globalSender); - var request = HttpRequest.newBuilder() - .uri(UriBuilder.fromUri(httpClient.signatureServiceRoot()).path(target.path(actualSender)).queryParam(POLLING_QUEUE_QUERY_PARAMETER, actualSender.getPollingQueue().value).build()) - .header(ACCEPT, APPLICATION_XML_TYPE.getType()) - .GET() - .build(); + try { - HttpResponse response = httpClient.httpClient().send(request, BodyHandlers.ofInputStream()); - StatusType status = ResponseStatus.resolve(response.statusCode()) - .throwIf(TOO_MANY_REQUESTS, s -> new TooEagerPollingException()) - .expect(SUCCESSFUL).orThrow(unexpectedStatus -> exceptionForGeneralError(response)); - return new JobStatusResponse<>(status == NO_CONTENT ? null : Marshalling.unmarshal(response.body(), responseClass), getNextPermittedPollTime(response)); + var uri = new URIBuilder(httpClient.signatureServiceRoot()) + .appendPath(target.path(actualSender)) + .addParameter(POLLING_QUEUE_QUERY_PARAMETER, actualSender.getPollingQueue().value) + .build(); + var get = ClassicRequestBuilder + .get(uri) + .addHeader(ACCEPT, APPLICATION_XML.getMimeType()) + .build(); + return httpClient.httpClient().execute(get, response -> { + var status = ResponseStatus.resolve(response.getCode()) + .throwIf(HttpStatus.SC_TOO_MANY_REQUESTS, s -> new TooEagerPollingException()) + .expect(StatusCodeFamily.SUCCESSFUL).orThrow(unexpectedStatus -> exceptionForGeneralError(response)); + return new JobStatusResponse<>(status.value() == HttpStatus.SC_NO_CONTENT ? null : Marshalling.unmarshal(response.getEntity().getContent(), responseClass), getNextPermittedPollTime(response)); + }); + } catch (URISyntaxException e) { + throw new RuntimeException("Could not create uri, because " + e.getMessage(), e); } catch (IOException e) { - throw new RuntimeException(e); - } catch (InterruptedException e) { - throw new RuntimeException(e); + throw new UncheckedIOException(e); } }); } - private static Instant getNextPermittedPollTime(HttpResponse response) { - return response.headers().firstValue(NEXT_PERMITTED_POLL_TIME_HEADER) - .map(nextPermittedPollTime -> ZonedDateTime.parse(nextPermittedPollTime, ISO_DATE_TIME).toInstant()) - .orElseThrow(() -> new RuntimeException("Expected header " + NEXT_PERMITTED_POLL_TIME_HEADER + " to exist")); + private static Instant getNextPermittedPollTime(ClassicHttpResponse response) throws ProtocolException { + return ZonedDateTime.parse(response.getHeader(NEXT_PERMITTED_POLL_TIME_HEADER).getValue(), ISO_DATE_TIME).toInstant(); } public void confirm(final Confirmable confirmable) { @@ -273,8 +257,11 @@ public void confirm(final Confirmable confirmable) { if (confirmable.getConfirmationReference() != null) { URI url = confirmable.getConfirmationReference().getConfirmationUrl(); LOG.info("Sends confirmation for '{}' to URL {}", confirmable, url); - var response = postEmptyEntity(url); - ResponseStatus.resolve(response.statusCode()).expect(SUCCESSFUL).orThrow(status -> exceptionForGeneralError(response)); + try (var response = postEmptyEntity(url)) { + ResponseStatus.resolve(response.getCode()).expect(StatusCodeFamily.SUCCESSFUL).orThrow(status -> exceptionForGeneralError(response)); + } catch (IOException e) { + throw new RuntimeException(e); + } } else { LOG.info("Does not need to send confirmation for '{}'", confirmable); } @@ -293,95 +280,91 @@ public void deleteDocuments(DeleteDocumentsUrl deleteDocumentsUrl) { call(() -> { if (deleteDocumentsUrl != null) { var url = deleteDocumentsUrl.getUrl(); - var response = delete(url); - ResponseStatus.resolve(response.statusCode()).expect(SUCCESSFUL).orThrow(status -> exceptionForGeneralError(response)); + try (var response = delete(url)) { + ResponseStatus.resolve(response.getCode()).expect(StatusCodeFamily.SUCCESSFUL).orThrow(status -> exceptionForGeneralError(response)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } else { throw new DocumentsNotDeletableException(); } }); } - private HttpResponse postEmptyEntity(URI uri) { + private ClassicHttpResponse postEmptyEntity(URI uri) { try { - var request = HttpRequest.newBuilder() - .uri(uri) - .header(ACCEPT, APPLICATION_XML_TYPE.getType()) - .POST(HttpRequest.BodyPublishers.noBody()) + var request = ClassicRequestBuilder + .post(uri) + .addHeader(ACCEPT, APPLICATION_XML.getMimeType()) .build(); - return httpClient.httpClient().send(request, BodyHandlers.ofInputStream()); + return httpClient.httpClient().execute(request, response -> response); } catch (IOException e) { throw new UncheckedIOException(e); - } catch (InterruptedException e) { - throw new RuntimeException(e); } } - private HttpResponse delete(URI uri) { + private ClassicHttpResponse delete(URI uri) { try { - var request = HttpRequest.newBuilder() - .uri(uri) - .header(ACCEPT, APPLICATION_XML_TYPE.getType()) - .DELETE() + var request = ClassicRequestBuilder + .delete(uri) + .addHeader(ACCEPT, APPLICATION_XML.getMimeType()) .build(); - return httpClient.httpClient().send(request, BodyHandlers.ofInputStream()); + return httpClient.httpClient().execute(request, response -> response); } catch (IOException e) { throw new UncheckedIOException(e); - } catch (InterruptedException e) { - throw new RuntimeException(e); } } - private static T parseResponse(HttpResponse response, Class responseType) { - ResponseStatus.resolve(response.statusCode()).expect(SUCCESSFUL).orThrow(unexpectedStatus -> exceptionForGeneralError(response)); - try(var body = response.body()) { + private static T parseResponse(ClassicHttpResponse response, Class responseType) { + ResponseStatus.resolve(response.getCode()).expect(StatusCodeFamily.SUCCESSFUL).orThrow(unexpectedStatus -> exceptionForGeneralError(response)); + try(var body = response.getEntity().getContent()) { return Marshalling.unmarshal(body, responseType); } catch (IOException e) { throw new UncheckedIOException("Could not parse response.", e); } } - private static SignatureException exceptionForGeneralError(HttpResponse response) { + private static SignatureException exceptionForGeneralError(ClassicHttpResponse response) { XMLError error = extractError(response); if (BROKER_NOT_AUTHORIZED.sameAs(error.getErrorCode())) { return new BrokerNotAuthorizedException(error); } - return new UnexpectedResponseException(error, ResponseStatus.resolve(response.statusCode()).get(), OK); + return new UnexpectedResponseException(error, ResponseStatus.resolve(response.getCode()).get(), StatusCode.from(HttpStatus.SC_OK)); } - private static XMLError extractError(HttpResponse response) { - XMLError error; - Optional responseContentType = response.headers().firstValue(HttpHeaders.CONTENT_TYPE); - if (responseContentType.isPresent() && MediaType.valueOf(responseContentType.get()).equals(APPLICATION_XML_TYPE)) { - try(var body = response.body()) { - error = Marshalling.unmarshal(body, XMLError.class); - } catch (IOException e) { - throw new UncheckedIOException("Could not extract error from body.", e); - } - } else { - String errorAsString; - try(var body = response.body()) { - ByteArrayOutputStream result = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - for (int length; (length = body.read(buffer)) != -1; ) { - result.write(buffer, 0, length); + private static XMLError extractError(ClassicHttpResponse response) { + try { + XMLError error; + var contentType = Optional.ofNullable(response.getHeader(HttpHeaders.CONTENT_TYPE)).map(NameValuePair::getValue).map(ContentType::create); + if (contentType.map(type -> type.equals(APPLICATION_XML)).orElse(false)) { + try(var body = response.getEntity().getContent()) { + error = Marshalling.unmarshal(body, XMLError.class); + } catch (IOException e) { + throw new UncheckedIOException("Could not extract error from body.", e); } - errorAsString = result.toString(StandardCharsets.UTF_8); - } catch (IOException e) { - // TODO: Kan tolke dette som en empty errorAsString? Dvs ignorere feilen? - throw new UncheckedIOException("Could not read body as string.", e); + } else { + String errorAsString; + try(var body = response.getEntity().getContent()) { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + for (int length; (length = body.read(buffer)) != -1; ) { + result.write(buffer, 0, length); + } + errorAsString = result.toString(StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException("Could not read body as string.", e); + } + throw new UnexpectedResponseException( + HttpHeaders.CONTENT_TYPE + " " + contentType.map(ContentType::getMimeType).orElse("unknown") + ": " + + Optional.ofNullable(errorAsString).filter(StringUtils::isNoneBlank).orElse(""), + ResponseStatus.resolve(response.getCode()).get(), StatusCode.from(HttpStatus.SC_OK)); } - throw new UnexpectedResponseException( - HttpHeaders.CONTENT_TYPE + " " + responseContentType.orElse("unknown") + ": " + - Optional.ofNullable(errorAsString).filter(StringUtils::isNoneBlank).orElse(""), - ResponseStatus.resolve(response.statusCode()).get(), OK); - } - // TODO: Dette skjer vel bare hvis vi ignorere IOExceptions? - if (error == null) { - throw new UnexpectedResponseException(null, ResponseStatus.resolve(response.statusCode()).get(), OK); + return error; + } catch (ProtocolException e) { + throw new RuntimeException(e); } - return error; } - + } diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/AddRequestHeaderFilter.java b/src/main/java/no/digipost/signature/client/core/internal/http/AddRequestHeaderFilter.java deleted file mode 100644 index 9f7c60af..00000000 --- a/src/main/java/no/digipost/signature/client/core/internal/http/AddRequestHeaderFilter.java +++ /dev/null @@ -1,27 +0,0 @@ -package no.digipost.signature.client.core.internal.http; - -import jakarta.annotation.Priority; -import jakarta.ws.rs.client.ClientRequestContext; -import jakarta.ws.rs.client.ClientRequestFilter; -import java.io.IOException; - -import static jakarta.ws.rs.Priorities.HEADER_DECORATOR; - - -@Priority(HEADER_DECORATOR) -public class AddRequestHeaderFilter implements ClientRequestFilter { - - private final String headerName; - private final String value; - - public AddRequestHeaderFilter(String headerName, String value) { - this.headerName = headerName; - this.value = value; - } - - @Override - public void filter(ClientRequestContext clientRequestContext) throws IOException { - clientRequestContext.getHeaders().add(headerName, value); - } - -} diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/HttpIntegrationConfiguration.java b/src/main/java/no/digipost/signature/client/core/internal/http/HttpIntegrationConfiguration.java index 983218b8..cca0b88d 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/http/HttpIntegrationConfiguration.java +++ b/src/main/java/no/digipost/signature/client/core/internal/http/HttpIntegrationConfiguration.java @@ -1,8 +1,8 @@ package no.digipost.signature.client.core.internal.http; +import org.apache.hc.client5.http.classic.HttpClient; + import java.net.URI; -import java.net.http.HttpClient; -import java.time.Duration; public interface HttpIntegrationConfiguration { @@ -10,6 +10,4 @@ public interface HttpIntegrationConfiguration { URI getServiceRoot(); - Duration socketTimeout(); - } diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/ResponseStatus.java b/src/main/java/no/digipost/signature/client/core/internal/http/ResponseStatus.java index be80ead7..6ac1cd89 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/http/ResponseStatus.java +++ b/src/main/java/no/digipost/signature/client/core/internal/http/ResponseStatus.java @@ -1,11 +1,8 @@ package no.digipost.signature.client.core.internal.http; +import no.digipost.signature.client.core.PollingQueue; import no.digipost.signature.client.core.exceptions.UnexpectedResponseException; -import jakarta.ws.rs.core.Response.Status; -import jakarta.ws.rs.core.Response.Status.Family; -import jakarta.ws.rs.core.Response.StatusType; - import java.util.Objects; import java.util.function.BiPredicate; import java.util.function.Function; @@ -14,37 +11,30 @@ public final class ResponseStatus { - public static ResponseStatus resolve(int code) { - StatusType status = Status.fromStatusCode(code); - if (status == null) { - status = Custom.fromStatusCode(code); - } - if (status == null) { - status = unknown(code); - } - return new ResponseStatus(status, s -> true); + public static ResponseStatus resolve(int statusCode) { + return new ResponseStatus(new StatusCode(statusCode), s -> true); } - private final StatusType status; - private final Predicate statusExpectation; + private final StatusCode statusCode; + private final Predicate statusExpectation; - private ResponseStatus(StatusType status, Predicate expectation) { - this.status = status; + private ResponseStatus(StatusCode statusCode, Predicate expectation) { + this.statusCode = statusCode; this.statusExpectation = expectation; } - public ResponseStatus expect(Status.Family expectedStatusFamily) { - return expect(s -> s.getFamily() == expectedStatusFamily); + public ResponseStatus expect(StatusCodeFamily expectedStatusFamily) { + return expect(s -> s.is(expectedStatusFamily)); } - public ResponseStatus expectOneOf(Status.Family ... expectedStatusFamilies) { - return expectOneOf(Stream.of(expectedStatusFamilies), (family, status) -> status.getFamily() == family); + public ResponseStatus expectOneOf(StatusCodeFamily ... expectedStatusFamilies) { + return expectOneOf(Stream.of(expectedStatusFamilies), (family, statusCode) -> statusCode.is(family)); } - private ResponseStatus expectOneOf(Stream expecteds, BiPredicate expectedEvaluator) { - Predicate oneOfExpectedsAndAlsoExistingExpectation = + private ResponseStatus expectOneOf(Stream expecteds, BiPredicate expectedEvaluator) { + Predicate oneOfExpectedsAndAlsoExistingExpectation = expecteds - .map(expected -> (Predicate) status -> expectedEvaluator.test(expected, status)) + .map(expected -> (Predicate) status -> expectedEvaluator.test(expected, status)) .reduce(Predicate::or) .map(statusExpectation::and) .orElse(statusExpectation); @@ -52,153 +42,43 @@ private ResponseStatus expectOneOf(Stream expecteds, BiPredicate expectation) { - return new ResponseStatus(status, this.statusExpectation.and(expectation)); + public ResponseStatus expect(Predicate expectation) { + return new ResponseStatus(statusCode, this.statusExpectation.and(expectation)); } - public ResponseStatus throwIf(Status status, Function exceptionSupplier) throws X { - return throwIf(s -> s.equals(status), exceptionSupplier); + public ResponseStatus throwIf(int status, Function exceptionSupplier) throws X { + return throwIf(s -> s.equals(StatusCode.from(status)), exceptionSupplier); } - public ResponseStatus throwIf(Predicate illegalStatus, Function exceptionSupplier) throws X { - if (illegalStatus.test(status)) { - throw exceptionSupplier.apply(status); + public ResponseStatus throwIf(Predicate illegalStatus, Function exceptionSupplier) throws X { + if (illegalStatus.test(statusCode)) { + throw exceptionSupplier.apply(statusCode); } else { return this; } } - public StatusType get() { - return orThrow(s -> new UnexpectedResponseException(status)); + public StatusCode get() { + return orThrow(s -> new UnexpectedResponseException(statusCode)); } - public StatusType orThrow(Function exceptionSuppplier) throws X { - return throwIf(statusExpectation.negate(), exceptionSuppplier).status; + public StatusCode orThrow(Function exceptionSupplier) throws X { + return throwIf(statusExpectation.negate(), exceptionSupplier).statusCode; } @Override public String toString() { - return status.toString() + (statusExpectation.test(status) ? "" : " (unexpected)"); + return statusCode.toString() + (statusExpectation.test(statusCode) ? "" : " (unexpected)"); } - - - - /** - * Status codes not part of the JAX-RS {@link Status} enum. - */ - public enum Custom implements StatusType { - - /** - * 422 Unprocessable Entity, see - * https://tools.ietf.org/html/rfc4918#section-11.2 - */ - UNPROCESSABLE_ENTITY(422, "Unprocessable Entity"), - - ; - - /** - * Convert a numerical status code into the corresponding CustomStatus. - * - * @param code the numerical status code. - * @return the matching Status or null is no matching Status is defined. - */ - public static ResponseStatus.Custom fromStatusCode(int code) { - for (Custom s : Custom.values()) { - if (s.code == code) { - return s; - } - } - return null; - } - - - - private int code; - private String reason; - private Family family; - - Custom(int code, String reasonPhrase) { - this.code = code; - this.reason = reasonPhrase; - this.family = Family.familyOf(code); - } - - @Override - public int getStatusCode() { - return code; - } - - @Override - public Family getFamily() { - return family; - } - - @Override - public String getReasonPhrase() { - return reason; - } - - @Override - public String toString() { - return code + " " + reason; - } - } - - - - static StatusType unknown(int code) { - return new Unknown(code); - } - - public static final class Unknown implements StatusType { - - final int code; - final Family family; - final String reason; - - private Unknown(int code) { - this.code = code; - this.family = Family.familyOf(code); - this.reason = "(" + family + ", unrecognized status code)"; - } - - - @Override - public int getStatusCode() { - return code; - } - - @Override - public String getReasonPhrase() { - return reason; - } - - @Override - public Family getFamily() { - return family; - } - - @Override - public String toString() { - return code + " " + reason; - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof ResponseStatus.Unknown) { - ResponseStatus.Unknown that = (ResponseStatus.Unknown) obj; - return Objects.equals(this.code, that.code); - } - return false; - } - - @Override - public int hashCode() { - return Objects.hash(code); + @Override + public boolean equals(Object obj) { + if (obj instanceof ResponseStatus) { + return Objects.equals(this.statusCode, ((ResponseStatus) obj).statusCode); } + return false; } } diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/SignatureApiTrustStrategy.java b/src/main/java/no/digipost/signature/client/core/internal/http/SignatureApiTrustStrategy.java index a3af6747..d1db4808 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/http/SignatureApiTrustStrategy.java +++ b/src/main/java/no/digipost/signature/client/core/internal/http/SignatureApiTrustStrategy.java @@ -3,7 +3,7 @@ import no.digipost.signature.client.core.exceptions.SecurityException; import no.digipost.signature.client.security.CertificateChainValidation; import no.digipost.signature.client.security.CertificateChainValidation.Result; -import org.apache.http.conn.ssl.TrustStrategy; +import org.apache.hc.core5.ssl.TrustStrategy; import java.security.cert.X509Certificate; diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClient.java b/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClient.java index 87178f2a..b790850e 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClient.java +++ b/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClient.java @@ -1,8 +1,8 @@ package no.digipost.signature.client.core.internal.http; +import org.apache.hc.client5.http.classic.HttpClient; + import java.net.URI; -import java.net.http.HttpClient; -import java.time.Duration; public interface SignatureHttpClient { @@ -10,6 +10,4 @@ public interface SignatureHttpClient { HttpClient httpClient(); - Duration socketTimeout(); - } diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClientFactory.java b/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClientFactory.java index bde42d39..7b017caa 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClientFactory.java +++ b/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClientFactory.java @@ -1,18 +1,13 @@ package no.digipost.signature.client.core.internal.http; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.hc.client5.http.classic.HttpClient; import java.net.URI; -import java.net.http.HttpClient; -import java.time.Duration; public class SignatureHttpClientFactory { - private static final Logger LOG = LoggerFactory.getLogger(SignatureHttpClientFactory.class); - public static SignatureHttpClient create(HttpIntegrationConfiguration config) { - return new DefaultClient(config.httpClient(), config.getServiceRoot(), config.socketTimeout()); + return new DefaultClient(config.httpClient(), config.getServiceRoot()); } @@ -21,12 +16,10 @@ private static final class DefaultClient implements SignatureHttpClient { private final HttpClient httpClient; private final URI signatureServiceRoot; - private final Duration socketTimeout; - DefaultClient(HttpClient httpClient, URI root, Duration socketTimeout) { + DefaultClient(HttpClient httpClient, URI root) { this.httpClient = httpClient; this.signatureServiceRoot = root; - this.socketTimeout = socketTimeout; } @Override @@ -38,9 +31,6 @@ public HttpClient httpClient() { return httpClient; } - public Duration socketTimeout() { - return socketTimeout; - } } } diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/StatusCode.java b/src/main/java/no/digipost/signature/client/core/internal/http/StatusCode.java new file mode 100644 index 00000000..ae804545 --- /dev/null +++ b/src/main/java/no/digipost/signature/client/core/internal/http/StatusCode.java @@ -0,0 +1,52 @@ +package no.digipost.signature.client.core.internal.http; + +import java.util.EnumSet; +import java.util.Objects; +import java.util.Set; + +public class StatusCode { + + private final int value; + + public static StatusCode from(int value) { + return new StatusCode(value); + } + + public StatusCode(int value) { + this.value = value; + } + + public boolean is(StatusCodeFamily family) { + return this.family() == family; + } + + public boolean isOneOf(StatusCodeFamily first, StatusCodeFamily ... rest) { + return isOneOf(EnumSet.of(first, rest)); + } + + public boolean isOneOf(Set families) { + return families.contains(this.family()); + } + + public StatusCodeFamily family() { + return StatusCodeFamily.of(value); + } + + public int value() { + return this.value; + } + + @Override + public String toString() { + return "status code " + value + " (" + family() + ")"; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof StatusCode) { + return Objects.equals(this.value, ((StatusCode) obj).value); + } + return false; + } + +} diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/StatusCodeFamily.java b/src/main/java/no/digipost/signature/client/core/internal/http/StatusCodeFamily.java new file mode 100644 index 00000000..be3dc0e5 --- /dev/null +++ b/src/main/java/no/digipost/signature/client/core/internal/http/StatusCodeFamily.java @@ -0,0 +1,22 @@ +package no.digipost.signature.client.core.internal.http; + +import org.apache.hc.core5.http.message.StatusLine; + +public enum StatusCodeFamily { + INFORMATIONAL, SUCCESSFUL, REDIRECTION, CLIENT_ERROR, SERVER_ERROR, OTHER; + + public static StatusCodeFamily of(StatusLine apacheHttpClientStatus) { + return of(apacheHttpClientStatus.getStatusCode()); + } + + public static StatusCodeFamily of(int statusCode) { + switch (statusCode / 100) { + case 1: return INFORMATIONAL; + case 2: return SUCCESSFUL; + case 3: return REDIRECTION; + case 4: return CLIENT_ERROR; + case 5: return SERVER_ERROR; + default: return OTHER; + } + } +} diff --git a/src/main/java/no/digipost/signature/client/direct/DirectClient.java b/src/main/java/no/digipost/signature/client/direct/DirectClient.java index d536bd0a..f634c8ba 100644 --- a/src/main/java/no/digipost/signature/client/direct/DirectClient.java +++ b/src/main/java/no/digipost/signature/client/direct/DirectClient.java @@ -17,14 +17,15 @@ import no.digipost.signature.client.core.internal.ClientHelper; import no.digipost.signature.client.core.internal.JobStatusResponse; import no.digipost.signature.client.core.internal.http.SignatureHttpClientFactory; +import org.apache.hc.core5.http.ContentType; import java.util.Optional; -import static jakarta.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE; -import static jakarta.ws.rs.core.MediaType.APPLICATION_XML_TYPE; import static no.digipost.signature.client.direct.DirectJobStatusResponse.noUpdatedStatus; import static no.digipost.signature.client.direct.JaxbEntityMapping.fromJaxb; import static no.digipost.signature.client.direct.JaxbEntityMapping.toJaxb; +import static org.apache.hc.core5.http.ContentType.APPLICATION_OCTET_STREAM; +import static org.apache.hc.core5.http.ContentType.APPLICATION_XML; public class DirectClient { @@ -132,11 +133,11 @@ public void confirm(DirectJobStatusResponse receivedStatusResponse) { public ResponseInputStream getXAdES(XAdESReference xAdESReference) { - return client.getDataStream(xAdESReference.getxAdESUrl(), APPLICATION_XML_TYPE); + return client.getDataStream(xAdESReference.getxAdESUrl(), APPLICATION_XML); } public ResponseInputStream getPAdES(PAdESReference pAdESReference) { - return client.getDataStream(pAdESReference.getpAdESUrl(), APPLICATION_OCTET_STREAM_TYPE, APPLICATION_XML_TYPE); + return client.getDataStream(pAdESReference.getpAdESUrl(), APPLICATION_OCTET_STREAM, APPLICATION_XML); } public void deleteDocuments(DeleteDocumentsUrl deleteDocumentsUrl) { diff --git a/src/main/java/no/digipost/signature/client/direct/StatusReference.java b/src/main/java/no/digipost/signature/client/direct/StatusReference.java index dc437a62..b2cd1ca8 100644 --- a/src/main/java/no/digipost/signature/client/direct/StatusReference.java +++ b/src/main/java/no/digipost/signature/client/direct/StatusReference.java @@ -1,8 +1,9 @@ package no.digipost.signature.client.direct; -import jakarta.ws.rs.core.UriBuilder; +import org.apache.hc.core5.net.URIBuilder; import java.net.URI; +import java.net.URISyntaxException; import java.util.Objects; /** * A {@code StatusReference} is constructed from the url acquired from @@ -54,7 +55,11 @@ private StatusReference(URI statusUrl, String statusQueryToken) { } public URI getStatusUrl() { - return UriBuilder.fromUri(statusUrl).queryParam(STATUS_QUERY_TOKEN_PARAM_NAME, statusQueryToken).build(); + try { + return new URIBuilder(statusUrl).addParameter(STATUS_QUERY_TOKEN_PARAM_NAME, statusQueryToken).build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } } public static abstract class StatusUrlContruction { diff --git a/src/main/java/no/digipost/signature/client/portal/PortalClient.java b/src/main/java/no/digipost/signature/client/portal/PortalClient.java index 7206d64a..e8bb2170 100644 --- a/src/main/java/no/digipost/signature/client/portal/PortalClient.java +++ b/src/main/java/no/digipost/signature/client/portal/PortalClient.java @@ -17,14 +17,15 @@ import no.digipost.signature.client.core.internal.ClientHelper; import no.digipost.signature.client.core.internal.JobStatusResponse; import no.digipost.signature.client.core.internal.http.SignatureHttpClientFactory; +import org.apache.hc.core5.http.ContentType; import java.util.Optional; -import static jakarta.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE; -import static jakarta.ws.rs.core.MediaType.APPLICATION_XML_TYPE; import static no.digipost.signature.client.portal.JaxbEntityMapping.fromJaxb; import static no.digipost.signature.client.portal.JaxbEntityMapping.toJaxb; import static no.digipost.signature.client.portal.PortalJobStatusChanged.noUpdatedStatus; +import static org.apache.hc.core5.http.ContentType.APPLICATION_OCTET_STREAM; +import static org.apache.hc.core5.http.ContentType.APPLICATION_XML; public class PortalClient { @@ -104,12 +105,12 @@ public void cancel(Cancellable cancellable) { public ResponseInputStream getXAdES(XAdESReference xAdESReference) { - return client.getDataStream(xAdESReference.getxAdESUrl(), APPLICATION_XML_TYPE); + return client.getDataStream(xAdESReference.getxAdESUrl(), APPLICATION_XML); } public ResponseInputStream getPAdES(PAdESReference pAdESReference) { - return client.getDataStream(pAdESReference.getpAdESUrl(), APPLICATION_OCTET_STREAM_TYPE, APPLICATION_XML_TYPE); + return client.getDataStream(pAdESReference.getpAdESUrl(), APPLICATION_OCTET_STREAM, APPLICATION_XML); } public void deleteDocuments(DeleteDocumentsUrl deleteDocumentsUrl) { diff --git a/src/test/java/no/digipost/signature/client/core/internal/http/ResponseStatusTest.java b/src/test/java/no/digipost/signature/client/core/internal/http/ResponseStatusTest.java index 032a6896..05239697 100644 --- a/src/test/java/no/digipost/signature/client/core/internal/http/ResponseStatusTest.java +++ b/src/test/java/no/digipost/signature/client/core/internal/http/ResponseStatusTest.java @@ -1,13 +1,8 @@ package no.digipost.signature.client.core.internal.http; -import nl.jqno.equalsverifier.EqualsVerifier; -import nl.jqno.equalsverifier.Warning; -import no.digipost.signature.client.core.internal.http.ResponseStatus.Custom; -import no.digipost.signature.client.core.internal.http.ResponseStatus.Unknown; +import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.Test; -import jakarta.ws.rs.core.Response.Status; - import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -17,18 +12,17 @@ public class ResponseStatusTest { @Test - public void resolveStandardHttpStatus() { - assertThat(ResponseStatus.resolve(200).get(), is(Status.OK)); - } - - @Test - public void resolveCustomHttpStatus() { - assertThat(ResponseStatus.resolve(422).get(), is(Custom.UNPROCESSABLE_ENTITY)); + public void resolveSuccessfulHttpStatus() { + assertThat(ResponseStatus.resolve(200).get().family(), is(StatusCodeFamily.SUCCESSFUL)); + assertThat(ResponseStatus.resolve(204).get().family(), is(StatusCodeFamily.SUCCESSFUL)); } @Test - public void resolveUnknownHttpStatus() { - assertThat(ResponseStatus.resolve(478).get(), is(ResponseStatus.unknown(478))); + public void resolveClientError() { + assertThat(ResponseStatus.resolve(400).get().family(), is(StatusCodeFamily.CLIENT_ERROR)); + assertThat(ResponseStatus.resolve(404).get().family(), is(StatusCodeFamily.CLIENT_ERROR)); + assertThat(ResponseStatus.resolve(409).get().family(), is(StatusCodeFamily.CLIENT_ERROR)); + assertThat(ResponseStatus.resolve(422).get().family(), is(StatusCodeFamily.CLIENT_ERROR)); } @Test @@ -41,8 +35,4 @@ public void correctEqualsHashCodeForAnyResolvedStatus() { }); } - @Test - public void correctEqualsHashCodeForUnknownStatus() { - EqualsVerifier.forClass(Unknown.class).suppress(Warning.ALL_FIELDS_SHOULD_BE_USED).verify(); - } } diff --git a/src/test/java/no/digipost/signature/client/core/internal/http/SignatureHttpClientFactoryTest.java b/src/test/java/no/digipost/signature/client/core/internal/http/SignatureHttpClientFactoryTest.java index c62e8a5f..ae01aaf8 100644 --- a/src/test/java/no/digipost/signature/client/core/internal/http/SignatureHttpClientFactoryTest.java +++ b/src/test/java/no/digipost/signature/client/core/internal/http/SignatureHttpClientFactoryTest.java @@ -1,10 +1,10 @@ package no.digipost.signature.client.core.internal.http; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.junit.jupiter.api.Test; import java.net.URI; -import java.net.http.HttpClient; -import java.time.Duration; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -20,7 +20,7 @@ void instantiatesSignatureHttpClient() { @Override public HttpClient httpClient() { - return HttpClient.newHttpClient(); + return HttpClientBuilder.create().build(); } @Override @@ -28,9 +28,4 @@ public URI getServiceRoot() { return URI.create("localhost"); } - @Override - public Duration socketTimeout() { - return Duration.ofSeconds(10); - } - } From f9f92b37d7a27180359c1d75cb7b4728442de76f Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Tue, 14 Feb 2023 10:35:16 +0100 Subject: [PATCH 10/32] Fix minor compiler warns --- .../digipost/signature/client/ClientConfiguration.java | 8 ++++---- .../client/asice/signature/CreateSignature.java | 4 +++- .../client/core/internal/http/ResponseStatus.java | 1 - .../digipost/signature/client/direct/DirectClient.java | 1 - .../digipost/signature/client/portal/PortalClient.java | 1 - .../client/asice/signature/CreateSignatureTest.java | 9 --------- .../client/core/internal/http/ResponseStatusTest.java | 1 - 7 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/main/java/no/digipost/signature/client/ClientConfiguration.java b/src/main/java/no/digipost/signature/client/ClientConfiguration.java index b76da294..2585e44a 100644 --- a/src/main/java/no/digipost/signature/client/ClientConfiguration.java +++ b/src/main/java/no/digipost/signature/client/ClientConfiguration.java @@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory; import javax.net.ssl.SSLContext; + import java.io.InputStream; import java.net.URI; import java.nio.file.Path; @@ -85,7 +86,6 @@ public final class ClientConfiguration implements ProvidesCertificateResourcePat private final KeyStoreConfig keyStoreConfig; private final org.apache.hc.client5.http.classic.HttpClient httpClient; - private final Duration socketTimeout; private final Optional sender; private final URI signatureServiceRoot; private final Iterable certificatePaths; @@ -93,12 +93,11 @@ public final class ClientConfiguration implements ProvidesCertificateResourcePat private final Clock clock; private ClientConfiguration( - KeyStoreConfig keyStoreConfig, org.apache.hc.client5.http.classic.HttpClient httpClient, Duration socketTimeout, Optional sender, + KeyStoreConfig keyStoreConfig, org.apache.hc.client5.http.classic.HttpClient httpClient, Optional sender, URI serviceRoot, Iterable certificatePaths, Iterable documentBundleProcessors, Clock clock) { this.keyStoreConfig = keyStoreConfig; this.httpClient = httpClient; - this.socketTimeout = socketTimeout; this.sender = sender; this.signatureServiceRoot = serviceRoot; this.certificatePaths = certificatePaths; @@ -144,6 +143,7 @@ public static Builder builder(KeyStoreConfig keystore) { return new Builder(keystore); } + @Override public Iterable getCertificatePaths() { return certificatePaths; } @@ -382,7 +382,7 @@ public ClientConfiguration build() { proxy.ifPresent(httpClientBuilder::setProxy); httpClientCustomizer.ifPresent(customizer -> customizer.accept(httpClientBuilder)); - return new ClientConfiguration(keyStoreConfig, httpClientBuilder.build(), socketTimeout, globalSender, serviceRoot, certificatePaths, documentBundleProcessors, clock); + return new ClientConfiguration(keyStoreConfig, httpClientBuilder.build(), globalSender, serviceRoot, certificatePaths, documentBundleProcessors, clock); } String createUserAgentString() { diff --git a/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java b/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java index 852f447e..0d1826a7 100644 --- a/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java +++ b/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java @@ -8,6 +8,7 @@ import no.digipost.signature.xsd.SignatureApiSchemas; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.Node; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; @@ -40,6 +41,7 @@ import javax.xml.transform.sax.SAXSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -195,7 +197,7 @@ protected Document createXmlSignature(final List { if (xadesArtifacts.signablePropertiesReferenceUri.equals(uriReference.getURI())) { - return (NodeSetData) domUtils.allNodesBelow(xadesArtifacts.signableProperties)::iterator; + return (NodeSetData) domUtils.allNodesBelow(xadesArtifacts.signableProperties)::iterator; } return signatureFactory.getURIDereferencer().dereference(uriReference, context); }; diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/ResponseStatus.java b/src/main/java/no/digipost/signature/client/core/internal/http/ResponseStatus.java index 6ac1cd89..8cf8ed6b 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/http/ResponseStatus.java +++ b/src/main/java/no/digipost/signature/client/core/internal/http/ResponseStatus.java @@ -1,6 +1,5 @@ package no.digipost.signature.client.core.internal.http; -import no.digipost.signature.client.core.PollingQueue; import no.digipost.signature.client.core.exceptions.UnexpectedResponseException; import java.util.Objects; diff --git a/src/main/java/no/digipost/signature/client/direct/DirectClient.java b/src/main/java/no/digipost/signature/client/direct/DirectClient.java index f634c8ba..c42f4a5f 100644 --- a/src/main/java/no/digipost/signature/client/direct/DirectClient.java +++ b/src/main/java/no/digipost/signature/client/direct/DirectClient.java @@ -17,7 +17,6 @@ import no.digipost.signature.client.core.internal.ClientHelper; import no.digipost.signature.client.core.internal.JobStatusResponse; import no.digipost.signature.client.core.internal.http.SignatureHttpClientFactory; -import org.apache.hc.core5.http.ContentType; import java.util.Optional; diff --git a/src/main/java/no/digipost/signature/client/portal/PortalClient.java b/src/main/java/no/digipost/signature/client/portal/PortalClient.java index e8bb2170..5fd0eca5 100644 --- a/src/main/java/no/digipost/signature/client/portal/PortalClient.java +++ b/src/main/java/no/digipost/signature/client/portal/PortalClient.java @@ -17,7 +17,6 @@ import no.digipost.signature.client.core.internal.ClientHelper; import no.digipost.signature.client.core.internal.JobStatusResponse; import no.digipost.signature.client.core.internal.http.SignatureHttpClientFactory; -import org.apache.hc.core5.http.ContentType; import java.util.Optional; diff --git a/src/test/java/no/digipost/signature/client/asice/signature/CreateSignatureTest.java b/src/test/java/no/digipost/signature/client/asice/signature/CreateSignatureTest.java index 327fe8c0..7b7df053 100644 --- a/src/test/java/no/digipost/signature/client/asice/signature/CreateSignatureTest.java +++ b/src/test/java/no/digipost/signature/client/asice/signature/CreateSignatureTest.java @@ -18,7 +18,6 @@ import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; -import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.transform.stream.StreamSource; @@ -49,14 +48,6 @@ public class CreateSignatureTest { private KeyStoreConfig noekkelpar; private List files; - private static final Marshaller marshaller; static { - try { - marshaller = JAXBContext.newInstance(XAdESSignatures.class, QualifyingProperties.class).createMarshaller(); - } catch (JAXBException e) { - throw new RuntimeException(e); - } - } - private static final Unmarshaller unmarshaller; static { try { unmarshaller = JAXBContext.newInstance(XAdESSignatures.class, QualifyingProperties.class).createUnmarshaller(); diff --git a/src/test/java/no/digipost/signature/client/core/internal/http/ResponseStatusTest.java b/src/test/java/no/digipost/signature/client/core/internal/http/ResponseStatusTest.java index 05239697..da92e18a 100644 --- a/src/test/java/no/digipost/signature/client/core/internal/http/ResponseStatusTest.java +++ b/src/test/java/no/digipost/signature/client/core/internal/http/ResponseStatusTest.java @@ -1,6 +1,5 @@ package no.digipost.signature.client.core.internal.http; -import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; From 2bd7cb37495af78dd92cd1d5db0a61cf8c3d59ad Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Tue, 14 Feb 2023 10:36:07 +0100 Subject: [PATCH 11/32] Change to non-deprecated java.security API getIssuerDN -> getIssuerX500Principal --- .../client/asice/signature/CreateXAdESArtifacts.java | 2 +- .../core/internal/http/SignatureApiTrustStrategy.java | 2 +- .../client/asice/signature/CreateSignatureTest.java | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/no/digipost/signature/client/asice/signature/CreateXAdESArtifacts.java b/src/main/java/no/digipost/signature/client/asice/signature/CreateXAdESArtifacts.java index 2ea05a09..810e9f1b 100644 --- a/src/main/java/no/digipost/signature/client/asice/signature/CreateXAdESArtifacts.java +++ b/src/main/java/no/digipost/signature/client/asice/signature/CreateXAdESArtifacts.java @@ -44,7 +44,7 @@ XAdESArtifacts createArtifactsToSign(List files } DigestAlgAndValueType certificateDigest = new DigestAlgAndValueType(sha1DigestMethod, certificateDigestValue); - X509IssuerSerialType certificateIssuer = new X509IssuerSerialType(certificate.getIssuerDN().getName(), certificate.getSerialNumber()); + X509IssuerSerialType certificateIssuer = new X509IssuerSerialType(certificate.getIssuerX500Principal().getName(), certificate.getSerialNumber()); SigningCertificate signingCertificate = new SigningCertificate(singletonList(new CertIDType(certificateDigest, certificateIssuer, null))); ZonedDateTime now = ZonedDateTime.now(clock); diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/SignatureApiTrustStrategy.java b/src/main/java/no/digipost/signature/client/core/internal/http/SignatureApiTrustStrategy.java index d1db4808..3ce6501a 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/http/SignatureApiTrustStrategy.java +++ b/src/main/java/no/digipost/signature/client/core/internal/http/SignatureApiTrustStrategy.java @@ -35,7 +35,7 @@ public boolean isTrusted(X509Certificate[] chain, String authType) { case TRUSTED_AND_SKIP_FURTHER_VALIDATION: return true; case TRUSTED: return false; case UNTRUSTED: default: - String subjectDN = chain[0].getSubjectDN().getName(); + String subjectDN = chain[0].getSubjectX500Principal().getName(); throw new SecurityException( "Untrusted server certificate, according to " + certificateChainValidation + ". " + "Make sure the server URI is correct. Actual certificate: " + subjectDN + ". " + diff --git a/src/test/java/no/digipost/signature/client/asice/signature/CreateSignatureTest.java b/src/test/java/no/digipost/signature/client/asice/signature/CreateSignatureTest.java index 7b7df053..69c8713b 100644 --- a/src/test/java/no/digipost/signature/client/asice/signature/CreateSignatureTest.java +++ b/src/test/java/no/digipost/signature/client/asice/signature/CreateSignatureTest.java @@ -75,10 +75,10 @@ public void test_generated_signatures() throws JAXBException { * If this changes, something has changed in the signature implementation, and must be investigated! */ final String expectedBase64EncodedSignatureValue = - "GELdAv4Dc+w5RnN3Hd2Fk9KjRh8LyNaiLM8PZLvFriuahHoZcb3OBTdCJhaxoup1tgmOUspMGgJ7Zgl6/hh" + - "G+zuZ0UjT521mqH4PrupH464B9ztBFnPScedvJpSAwROqSn7eCG78J+ittKBhKxPkBmfStaGPxnvqm2reug" + - "I3vbFem+KDiFU+Q4T26OWXapLQC2fhEttH/pUYND1PZN8pNaMiKgzaG7aHurMQCoB5qxfKEL9YEe4pF7H0T" + - "PAdNndCACJiWrkHNQ5gTJ+UWx8y2kuzZHEGGTJ+ip9KpCRohDfLapQAMTh0zMLrUNbYpq6kiYWrlxTNfdcVm4skBY0j9Q=="; + "L3HFN44OGUEbK5p9zxbnDZrey+UnQ9fYQX0k7gv8hfxouRfXFvNHXtUJEI00/BOlhcyGRu8wEIpKYEkDIzj" + + "WZyKXjV8Tz6PkHMJedgVGFDPlKwkx7gufntbjH2xqhBOsJzobDQ44rqOlK1YiXNQCPAMFwN/CpOTQTRFuf9" + + "/37BN2QG5cmgz+ZNqcKPwQnjrVaQBOrQEc5D2/n05aPsRdc6OUzu2TIftoRLRH1peRDLCAo7MjPNhYo1CCi" + + "nBa0FMipG5jtqUPYJJMTt56wIJlwQ95PhGIEYtdRYwxlgSau9Bw+wYmD4NU0K0hw6FgBQ/UDF87T5Zr7HTPWPMwpngkWg=="; Signature signature = createSignature.createSignature(files, noekkelpar); XAdESSignatures xAdESSignatures = (XAdESSignatures) unmarshaller.unmarshal(new StreamSource(new ByteArrayInputStream(signature.getContent()))); @@ -137,7 +137,7 @@ private void verify_signing_certificate(final SigningCertificate signingCertific assertThat(certDigest.getDigestValue().length, is(20)); // SHA1 is 160 bits => 20 bytes X509IssuerSerialType issuerSerial = signingCertificate.getCerts().get(0).getIssuerSerial(); - assertThat(issuerSerial.getX509IssuerName(), is("CN=Avsender, OU=Avsender, O=Avsender, L=Oslo, ST=NO, C=NO")); + assertThat(issuerSerial.getX509IssuerName(), is("CN=Avsender,OU=Avsender,O=Avsender,L=Oslo,ST=NO,C=NO")); assertThat(issuerSerial.getX509SerialNumber(), is(new BigInteger("589725471"))); } From 2247bec25e1e1450dbe512a3f6eb7a7981946221 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Tue, 14 Feb 2023 13:16:34 +0100 Subject: [PATCH 12/32] Do not close response stream for downloads --- .../client/core/internal/ClientHelper.java | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java index ae81b1df..18d8c76b 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java @@ -66,6 +66,7 @@ import static no.digipost.signature.client.core.internal.ErrorCodes.SIGNING_CEREMONY_NOT_COMPLETED; import static no.digipost.signature.client.core.internal.Target.DIRECT; import static no.digipost.signature.client.core.internal.Target.PORTAL; +import static no.digipost.signature.client.core.internal.http.StatusCodeFamily.SUCCESSFUL; import static org.apache.hc.core5.http.ContentType.APPLICATION_OCTET_STREAM; import static org.apache.hc.core5.http.ContentType.APPLICATION_XML; import static org.apache.hc.core5.http.ContentType.MULTIPART_MIXED; @@ -179,19 +180,31 @@ public XMLDirectSignatureJobStatusResponse sendSignatureJobStatusRequest(final U public ResponseInputStream getDataStream(URI uri, ContentType ... acceptedResponses) { return call(() -> { - var request = new HttpGet(uri); + var request = ClassicRequestBuilder.get(uri).build(); Arrays.stream(acceptedResponses).forEach(mediaType -> request.addHeader(ACCEPT, mediaType.getMimeType())); + ClassicHttpResponse response = null; try { - return httpClient.httpClient().execute(request, response -> { - ResponseStatus.resolve(response.getCode()).expect(StatusCodeFamily.SUCCESSFUL).orThrow(unexpectedStatus -> exceptionForGeneralError(response)); - return new ResponseInputStream( - response.getEntity().getContent(), - Math.toIntExact(response.getEntity().getContentLength())); - }); - } catch (IOException e) { - throw new UncheckedIOException(e); + response = httpClient.httpClient().execute(null, request); + var statusCode = StatusCode.from(response.getCode()); + if (!statusCode.is(SUCCESSFUL)) { + throw exceptionForGeneralError(response); + } + return new ResponseInputStream( + response.getEntity().getContent(), + Math.toIntExact(response.getEntity().getContentLength())); + } catch (Exception e) { + if (response != null) { + try { + response.close(); + } catch (IOException closingException) { + e.addSuppressed(closingException); + } + } + throw e instanceof RuntimeException + ? (RuntimeException) e + : new RuntimeException("uri: " + uri + ": " + e.getClass().getSimpleName() + " '" + e.getMessage() + "'", e); } }); } From 2bc85afd7a9430f279530b8db03efc348f0bf25a Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Tue, 14 Feb 2023 15:38:21 +0100 Subject: [PATCH 13/32] Fix wrong AND operator Fixing my own 2 years old buggy supposedly quality-of-life enhancements --- .../client/core/exceptions/UnexpectedResponseException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/no/digipost/signature/client/core/exceptions/UnexpectedResponseException.java b/src/main/java/no/digipost/signature/client/core/exceptions/UnexpectedResponseException.java index 16db824d..bced1aab 100644 --- a/src/main/java/no/digipost/signature/client/core/exceptions/UnexpectedResponseException.java +++ b/src/main/java/no/digipost/signature/client/core/exceptions/UnexpectedResponseException.java @@ -45,7 +45,7 @@ public String getErrorType() { } public boolean isErrorCode(String errorCode) { - return error != null & Objects.equals(error.getErrorCode(), errorCode); + return error != null && Objects.equals(error.getErrorCode(), errorCode); } private static String prettyprintExpectedStatuses(StatusCode ... statuses) { From b82d584118eb8dcf68325df4d28e800759e8f107 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Tue, 14 Feb 2023 16:09:50 +0100 Subject: [PATCH 14/32] Fix multiple values for Accept HTTP header --- .../signature/client/core/internal/ClientHelper.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java index 18d8c76b..58b4e188 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java @@ -44,6 +44,8 @@ import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; +import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.hc.core5.http.message.HeaderGroup; import org.apache.hc.core5.net.URIBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,9 +58,9 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.ZonedDateTime; -import java.util.Arrays; import java.util.Optional; import java.util.function.Supplier; +import java.util.stream.Stream; import static java.time.format.DateTimeFormatter.ISO_DATE_TIME; import static no.digipost.signature.client.core.internal.ActualSender.getActualSender; @@ -180,9 +182,9 @@ public XMLDirectSignatureJobStatusResponse sendSignatureJobStatusRequest(final U public ResponseInputStream getDataStream(URI uri, ContentType ... acceptedResponses) { return call(() -> { - var request = ClassicRequestBuilder.get(uri).build(); - - Arrays.stream(acceptedResponses).forEach(mediaType -> request.addHeader(ACCEPT, mediaType.getMimeType())); + var acceptHeader = new HeaderGroup(); + Stream.of(acceptedResponses).map(ContentType::getMimeType).map(acceptedMimeType -> new BasicHeader(ACCEPT, acceptedMimeType)).forEach(acceptHeader::addHeader); + var request = ClassicRequestBuilder.get(uri).addHeader(acceptHeader.getCondensedHeader(ACCEPT)).build(); ClassicHttpResponse response = null; try { From 2b8b2fbfaedf8ace1b745185f890deffd11dea23 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Tue, 14 Feb 2023 16:11:24 +0100 Subject: [PATCH 15/32] Fix identifying content-type of error response --- .../digipost/signature/client/core/internal/ClientHelper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java index 58b4e188..204b3e07 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java @@ -352,8 +352,8 @@ private static SignatureException exceptionForGeneralError(ClassicHttpResponse r private static XMLError extractError(ClassicHttpResponse response) { try { XMLError error; - var contentType = Optional.ofNullable(response.getHeader(HttpHeaders.CONTENT_TYPE)).map(NameValuePair::getValue).map(ContentType::create); - if (contentType.map(type -> type.equals(APPLICATION_XML)).orElse(false)) { + var contentType = Optional.ofNullable(response.getHeader(CONTENT_TYPE)).map(NameValuePair::getValue).map(ContentType::create); + if (contentType.filter(APPLICATION_XML::isSameMimeType).isPresent()) { try(var body = response.getEntity().getContent()) { error = Marshalling.unmarshal(body, XMLError.class); } catch (IOException e) { From 99d652a9f18ed71d2d4b07b5322c4638550534fa Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Tue, 14 Feb 2023 16:14:32 +0100 Subject: [PATCH 16/32] Do not close responses not consumed yet --- .../digipost/signature/client/core/internal/ClientHelper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java index 204b3e07..258ef6b7 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java @@ -313,7 +313,7 @@ private ClassicHttpResponse postEmptyEntity(URI uri) { .addHeader(ACCEPT, APPLICATION_XML.getMimeType()) .build(); - return httpClient.httpClient().execute(request, response -> response); + return httpClient.httpClient().execute(null, request); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -326,7 +326,7 @@ private ClassicHttpResponse delete(URI uri) { .addHeader(ACCEPT, APPLICATION_XML.getMimeType()) .build(); - return httpClient.httpClient().execute(request, response -> response); + return httpClient.httpClient().execute(null, request); } catch (IOException e) { throw new UncheckedIOException(e); } From b715c953d3363ee645b79dda24198b71c2d3fe65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20St=C3=B8a?= Date: Wed, 15 Feb 2023 09:19:29 +0100 Subject: [PATCH 17/32] Support java 8 again --- .github/workflows/build.yml | 2 +- .java-version | 2 +- pom.xml | 4 +- .../signature/client/ClientConfiguration.java | 6 +-- .../asice/signature/CreateSignature.java | 9 ++-- .../client/core/internal/ClientHelper.java | 44 ++++++++++--------- 6 files changed, 34 insertions(+), 33 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 13be63ef..15f4b61b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '11' ] + java: [ '8', '11' ] name: build java ${{ matrix.java }} steps: diff --git a/.java-version b/.java-version index b4de3947..62593409 100644 --- a/.java-version +++ b/.java-version @@ -1 +1 @@ -11 +1.8 diff --git a/pom.xml b/pom.xml index cf6eac5f..f5d5def2 100644 --- a/pom.xml +++ b/pom.xml @@ -14,8 +14,8 @@ - 11 - 11 + 1.8 + 1.8 set_with_-Dproject.previousVersion=X.Y remove-spring-dependency-SNAPSHOT 1.7.36 diff --git a/src/main/java/no/digipost/signature/client/ClientConfiguration.java b/src/main/java/no/digipost/signature/client/ClientConfiguration.java index 2585e44a..ede182bc 100644 --- a/src/main/java/no/digipost/signature/client/ClientConfiguration.java +++ b/src/main/java/no/digipost/signature/client/ClientConfiguration.java @@ -367,14 +367,14 @@ public Builder usingClock(Clock clock) { public ClientConfiguration build() { // TODO: Add possibility to add logger for http client - var socketConfig = SocketConfig.custom().setSoTimeout(Timeout.ofMilliseconds(socketTimeout.toMillis())).build(); - var poolingHttpClientConnectionManager = PoolingHttpClientConnectionManagerBuilder.create() + SocketConfig socketConfig = SocketConfig.custom().setSoTimeout(Timeout.ofMilliseconds(socketTimeout.toMillis())).build(); + PoolingHttpClientConnectionManagerBuilder poolingHttpClientConnectionManager = PoolingHttpClientConnectionManagerBuilder.create() .setDefaultSocketConfig(socketConfig) .setSSLSocketFactory(SSLConnectionSocketFactoryBuilder.create() .setSslContext(sslContext()) .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) .build()); - var requestConfigBuilder = RequestConfig.custom().setConnectionRequestTimeout(Timeout.ofMilliseconds(connectTimeout.toMillis())); + RequestConfig.Builder requestConfigBuilder = RequestConfig.custom().setConnectionRequestTimeout(Timeout.ofMilliseconds(connectTimeout.toMillis())); httpClientBuilder .setConnectionManager(poolingHttpClientConnectionManager.build()) .setDefaultRequestConfig(requestConfigBuilder.build()) diff --git a/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java b/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java index 0d1826a7..9a09286d 100644 --- a/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java +++ b/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java @@ -8,7 +8,6 @@ import no.digipost.signature.xsd.SignatureApiSchemas; import org.w3c.dom.Document; import org.w3c.dom.Element; -import org.w3c.dom.Node; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; @@ -41,7 +40,6 @@ import javax.xml.transform.sax.SAXSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -55,9 +53,10 @@ import java.security.cert.Certificate; import java.time.Clock; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.List; -import java.util.Set; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -138,7 +137,7 @@ private static Schema createSchema(Collection resources) { } private Schema loadSchema() { - return createSchema(Set.of(SignatureApiSchemas.XMLDSIG_SCHEMA, SignatureApiSchemas.ASICE_SCHEMA)); + return createSchema(new HashSet<>(Arrays.asList(SignatureApiSchemas.XMLDSIG_SCHEMA, SignatureApiSchemas.ASICE_SCHEMA))); } public Signature createSignature(final List attachedFiles, final KeyStoreConfig keyStoreConfig) { @@ -197,7 +196,7 @@ protected Document createXmlSignature(final List { if (xadesArtifacts.signablePropertiesReferenceUri.equals(uriReference.getURI())) { - return (NodeSetData) domUtils.allNodesBelow(xadesArtifacts.signableProperties)::iterator; + return (NodeSetData) domUtils.allNodesBelow(xadesArtifacts.signableProperties)::iterator; } return signatureFactory.getURIDereferencer().dereference(uriReference, context); }; diff --git a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java index 258ef6b7..3854dc2d 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java @@ -36,6 +36,7 @@ import org.apache.hc.client5.http.entity.mime.InputStreamBody; import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; import org.apache.hc.client5.http.entity.mime.MultipartPartBuilder; +import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpEntity; @@ -52,6 +53,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.UncheckedIOException; import java.net.URI; import java.net.URISyntaxException; @@ -107,7 +109,7 @@ public XMLPortalSignatureJobResponse sendPortalSignatureJobRequest(XMLPortalSign private RESPONSE multipartSignatureJobRequest(REQUEST signatureJobRequest, DocumentBundle documentBundle, Sender actualSender, Target target, Class responseClass) { return call(() -> { try { - var multipartEntityBuilder = MultipartEntityBuilder.create(); + MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create(); multipartEntityBuilder.setContentType(MULTIPART_MIXED); try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { @@ -122,7 +124,7 @@ private RESPONSE multipartSignatureJobRequest(REQUEST signat .addHeader(CONTENT_TYPE, APPLICATION_OCTET_STREAM.getMimeType()).build()); try (HttpEntity multiPart = multipartEntityBuilder.build()) { - var request = ClassicRequestBuilder + ClassicHttpRequest request = ClassicRequestBuilder .post(new URIBuilder(httpClient.signatureServiceRoot()).appendPath(target.path(actualSender)).build()) .addHeader(ACCEPT, APPLICATION_XML.getMimeType()) .build(); @@ -142,7 +144,7 @@ private RESPONSE multipartSignatureJobRequest(REQUEST signat public XMLDirectSignerResponse requestNewRedirectUrl(WithSignerUrl url) { try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { Marshalling.marshal(new XMLDirectSignerUpdateRequest().withRedirectUrl(new XMLEmptyElement()), os); - var request = new HttpPost(url.getSignerUrl()); + ClassicHttpRequest request = new HttpPost(url.getSignerUrl()); request.addHeader(ACCEPT, APPLICATION_XML.getMimeType()); return httpClient.httpClient().execute(request, response -> parseResponse(response, XMLDirectSignerResponse.class)); @@ -153,7 +155,7 @@ public XMLDirectSignerResponse requestNewRedirectUrl(WithSignerUrl url) { public XMLDirectSignatureJobStatusResponse sendSignatureJobStatusRequest(final URI statusUrl) { return call(() -> { - var request = new HttpGet(statusUrl); + ClassicHttpRequest request = new HttpGet(statusUrl); request.addHeader(ACCEPT, APPLICATION_XML.getMimeType()); try { @@ -182,14 +184,14 @@ public XMLDirectSignatureJobStatusResponse sendSignatureJobStatusRequest(final U public ResponseInputStream getDataStream(URI uri, ContentType ... acceptedResponses) { return call(() -> { - var acceptHeader = new HeaderGroup(); + HeaderGroup acceptHeader = new HeaderGroup(); Stream.of(acceptedResponses).map(ContentType::getMimeType).map(acceptedMimeType -> new BasicHeader(ACCEPT, acceptedMimeType)).forEach(acceptHeader::addHeader); - var request = ClassicRequestBuilder.get(uri).addHeader(acceptHeader.getCondensedHeader(ACCEPT)).build(); + ClassicHttpRequest request = ClassicRequestBuilder.get(uri).addHeader(acceptHeader.getCondensedHeader(ACCEPT)).build(); ClassicHttpResponse response = null; try { response = httpClient.httpClient().execute(null, request); - var statusCode = StatusCode.from(response.getCode()); + StatusCode statusCode = StatusCode.from(response.getCode()); if (!statusCode.is(SUCCESSFUL)) { throw exceptionForGeneralError(response); } @@ -214,7 +216,7 @@ public ResponseInputStream getDataStream(URI uri, ContentType ... acceptedRespon public void cancel(final Cancellable cancellable) { call(() -> { if (cancellable.getCancellationUrl() != null) { - try(var response = postEmptyEntity(cancellable.getCancellationUrl().getUrl())) { + try(ClassicHttpResponse response = postEmptyEntity(cancellable.getCancellationUrl().getUrl())) { ResponseStatus.resolve(response.getCode()) .throwIf(HttpStatus.SC_CONFLICT, status -> new JobCannotBeCancelledException(status, extractError(response))) .expect(StatusCodeFamily.SUCCESSFUL) @@ -241,16 +243,16 @@ private JobStatusResponse getStatusChange(final Sender actualSender = getActualSender(sender, globalSender); try { - var uri = new URIBuilder(httpClient.signatureServiceRoot()) + URI uri = new URIBuilder(httpClient.signatureServiceRoot()) .appendPath(target.path(actualSender)) .addParameter(POLLING_QUEUE_QUERY_PARAMETER, actualSender.getPollingQueue().value) .build(); - var get = ClassicRequestBuilder + ClassicHttpRequest get = ClassicRequestBuilder .get(uri) .addHeader(ACCEPT, APPLICATION_XML.getMimeType()) .build(); return httpClient.httpClient().execute(get, response -> { - var status = ResponseStatus.resolve(response.getCode()) + StatusCode status = ResponseStatus.resolve(response.getCode()) .throwIf(HttpStatus.SC_TOO_MANY_REQUESTS, s -> new TooEagerPollingException()) .expect(StatusCodeFamily.SUCCESSFUL).orThrow(unexpectedStatus -> exceptionForGeneralError(response)); return new JobStatusResponse<>(status.value() == HttpStatus.SC_NO_CONTENT ? null : Marshalling.unmarshal(response.getEntity().getContent(), responseClass), getNextPermittedPollTime(response)); @@ -272,7 +274,7 @@ public void confirm(final Confirmable confirmable) { if (confirmable.getConfirmationReference() != null) { URI url = confirmable.getConfirmationReference().getConfirmationUrl(); LOG.info("Sends confirmation for '{}' to URL {}", confirmable, url); - try (var response = postEmptyEntity(url)) { + try (ClassicHttpResponse response = postEmptyEntity(url)) { ResponseStatus.resolve(response.getCode()).expect(StatusCodeFamily.SUCCESSFUL).orThrow(status -> exceptionForGeneralError(response)); } catch (IOException e) { throw new RuntimeException(e); @@ -294,8 +296,8 @@ private void call(Runnable action) { public void deleteDocuments(DeleteDocumentsUrl deleteDocumentsUrl) { call(() -> { if (deleteDocumentsUrl != null) { - var url = deleteDocumentsUrl.getUrl(); - try (var response = delete(url)) { + URI url = deleteDocumentsUrl.getUrl(); + try (ClassicHttpResponse response = delete(url)) { ResponseStatus.resolve(response.getCode()).expect(StatusCodeFamily.SUCCESSFUL).orThrow(status -> exceptionForGeneralError(response)); } catch (IOException e) { throw new UncheckedIOException(e); @@ -308,7 +310,7 @@ public void deleteDocuments(DeleteDocumentsUrl deleteDocumentsUrl) { private ClassicHttpResponse postEmptyEntity(URI uri) { try { - var request = ClassicRequestBuilder + ClassicHttpRequest request = ClassicRequestBuilder .post(uri) .addHeader(ACCEPT, APPLICATION_XML.getMimeType()) .build(); @@ -321,7 +323,7 @@ private ClassicHttpResponse postEmptyEntity(URI uri) { private ClassicHttpResponse delete(URI uri) { try { - var request = ClassicRequestBuilder + ClassicHttpRequest request = ClassicRequestBuilder .delete(uri) .addHeader(ACCEPT, APPLICATION_XML.getMimeType()) .build(); @@ -334,7 +336,7 @@ private ClassicHttpResponse delete(URI uri) { private static T parseResponse(ClassicHttpResponse response, Class responseType) { ResponseStatus.resolve(response.getCode()).expect(StatusCodeFamily.SUCCESSFUL).orThrow(unexpectedStatus -> exceptionForGeneralError(response)); - try(var body = response.getEntity().getContent()) { + try(InputStream body = response.getEntity().getContent()) { return Marshalling.unmarshal(body, responseType); } catch (IOException e) { throw new UncheckedIOException("Could not parse response.", e); @@ -352,22 +354,22 @@ private static SignatureException exceptionForGeneralError(ClassicHttpResponse r private static XMLError extractError(ClassicHttpResponse response) { try { XMLError error; - var contentType = Optional.ofNullable(response.getHeader(CONTENT_TYPE)).map(NameValuePair::getValue).map(ContentType::create); + Optional contentType = Optional.ofNullable(response.getHeader(CONTENT_TYPE)).map(NameValuePair::getValue).map(ContentType::create); if (contentType.filter(APPLICATION_XML::isSameMimeType).isPresent()) { - try(var body = response.getEntity().getContent()) { + try(InputStream body = response.getEntity().getContent()) { error = Marshalling.unmarshal(body, XMLError.class); } catch (IOException e) { throw new UncheckedIOException("Could not extract error from body.", e); } } else { String errorAsString; - try(var body = response.getEntity().getContent()) { + try(InputStream body = response.getEntity().getContent()) { ByteArrayOutputStream result = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; for (int length; (length = body.read(buffer)) != -1; ) { result.write(buffer, 0, length); } - errorAsString = result.toString(StandardCharsets.UTF_8); + errorAsString = result.toString(StandardCharsets.UTF_8.name()); } catch (IOException e) { throw new UncheckedIOException("Could not read body as string.", e); } From fbeb763d6f4348de1946c7f2353d202566c81414 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Wed, 15 Feb 2023 21:19:17 +0100 Subject: [PATCH 18/32] Change contentLength from int to long --- .../digipost/signature/client/core/ResponseInputStream.java | 6 +++--- .../signature/client/core/internal/ClientHelper.java | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/no/digipost/signature/client/core/ResponseInputStream.java b/src/main/java/no/digipost/signature/client/core/ResponseInputStream.java index 85427c4a..4f203256 100644 --- a/src/main/java/no/digipost/signature/client/core/ResponseInputStream.java +++ b/src/main/java/no/digipost/signature/client/core/ResponseInputStream.java @@ -5,14 +5,14 @@ public class ResponseInputStream extends FilterInputStream { - private final int contentLength; + private final long contentLength; - public ResponseInputStream(InputStream in, int contentLength) { + public ResponseInputStream(InputStream in, long contentLength) { super(in); this.contentLength = contentLength; } - public int getContentLength() { + public long getContentLength() { return contentLength; } } diff --git a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java index 3854dc2d..4ceb4b7f 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java @@ -195,9 +195,7 @@ public ResponseInputStream getDataStream(URI uri, ContentType ... acceptedRespon if (!statusCode.is(SUCCESSFUL)) { throw exceptionForGeneralError(response); } - return new ResponseInputStream( - response.getEntity().getContent(), - Math.toIntExact(response.getEntity().getContentLength())); + return new ResponseInputStream(response.getEntity().getContent(), response.getEntity().getContentLength()); } catch (Exception e) { if (response != null) { try { From 8812d5adda82274e7272dfe3a3e8521866dda7c0 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Wed, 15 Feb 2023 21:21:15 +0100 Subject: [PATCH 19/32] Use ContentType.parse to parse content type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixing wrong use of ContentType.create, which really is a factory method specializd for strings you _know_ is just a MIME-type, and fails if you provide e.g. a content-type with a charset. In summary: ContentType.create() 👎 ContentType.parse() 👍 Naming things is hard. --- .../digipost/signature/client/core/internal/ClientHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java index 4ceb4b7f..b3d8266f 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java @@ -352,7 +352,7 @@ private static SignatureException exceptionForGeneralError(ClassicHttpResponse r private static XMLError extractError(ClassicHttpResponse response) { try { XMLError error; - Optional contentType = Optional.ofNullable(response.getHeader(CONTENT_TYPE)).map(NameValuePair::getValue).map(ContentType::create); + Optional contentType = Optional.ofNullable(response.getHeader(CONTENT_TYPE)).map(NameValuePair::getValue).map(ContentType::parse); if (contentType.filter(APPLICATION_XML::isSameMimeType).isPresent()) { try(InputStream body = response.getEntity().getContent()) { error = Marshalling.unmarshal(body, XMLError.class); From d078d8583dd74f2d61d56deac53f565c3062f540 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 16 Feb 2023 12:27:39 +0100 Subject: [PATCH 20/32] Rename statusCode to httpStatusCode --- .../core/exceptions/UnexpectedResponseException.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/no/digipost/signature/client/core/exceptions/UnexpectedResponseException.java b/src/main/java/no/digipost/signature/client/core/exceptions/UnexpectedResponseException.java index bced1aab..7dc2b9f5 100644 --- a/src/main/java/no/digipost/signature/client/core/exceptions/UnexpectedResponseException.java +++ b/src/main/java/no/digipost/signature/client/core/exceptions/UnexpectedResponseException.java @@ -8,7 +8,7 @@ public class UnexpectedResponseException extends SignatureException { private final XMLError error; - private final int statusCode; + private final int httpStatusCode; public UnexpectedResponseException(StatusCode actual) { this(null, actual); @@ -25,11 +25,15 @@ public UnexpectedResponseException(Object errorEntity, Throwable cause, StatusCo (cause != null ? " - " + cause.getClass().getSimpleName() + ": '" + cause.getMessage() + "'.": ""), cause); this.error = errorEntity instanceof XMLError ? (XMLError) errorEntity : null; - this.statusCode = actual.value(); + this.httpStatusCode = actual.value(); } - public boolean isStatusCode(int statusCode) { - return this.statusCode == statusCode; + public boolean isHttpStatusCode(int statusCode) { + return this.httpStatusCode == statusCode; + } + + public int httpStatusCode() { + return httpStatusCode; } public String getErrorCode() { From b3ecccb170a399ec5ef58a25f2669f4c4a22602c Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 16 Feb 2023 13:03:34 +0100 Subject: [PATCH 21/32] Add facility to build URLs based on serviceRoot To get rid of that pesky checked URISyntaxException creeping in everywhere. --- .../client/core/internal/ClientHelper.java | 38 +++++++++---------- .../internal/http/SignatureHttpClient.java | 15 ++++++++ 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java index b3d8266f..5a04492e 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java @@ -47,7 +47,6 @@ import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.http.message.HeaderGroup; -import org.apache.hc.core5.net.URIBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,13 +55,11 @@ import java.io.InputStream; import java.io.UncheckedIOException; import java.net.URI; -import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.ZonedDateTime; import java.util.Optional; import java.util.function.Supplier; -import java.util.stream.Stream; import static java.time.format.DateTimeFormatter.ISO_DATE_TIME; import static no.digipost.signature.client.core.internal.ActualSender.getActualSender; @@ -125,7 +122,7 @@ private RESPONSE multipartSignatureJobRequest(REQUEST signat try (HttpEntity multiPart = multipartEntityBuilder.build()) { ClassicHttpRequest request = ClassicRequestBuilder - .post(new URIBuilder(httpClient.signatureServiceRoot()).appendPath(target.path(actualSender)).build()) + .post(httpClient.constructUrl(uri -> uri.appendPath(target.path(actualSender)))) .addHeader(ACCEPT, APPLICATION_XML.getMimeType()) .build(); @@ -135,8 +132,6 @@ private RESPONSE multipartSignatureJobRequest(REQUEST signat } } catch (IOException e) { throw new UncheckedIOException(e); - } catch (URISyntaxException e) { - throw new RuntimeException(e); } }); } @@ -182,11 +177,19 @@ public XMLDirectSignatureJobStatusResponse sendSignatureJobStatusRequest(final U }); } - public ResponseInputStream getDataStream(URI uri, ContentType ... acceptedResponses) { + public ResponseInputStream getDataStream(URI absoluteUri, ContentType ... acceptedResponses) { + if (!absoluteUri.isAbsolute()) { + throw new IllegalArgumentException("'" + absoluteUri + "' is not an absolute URL"); + } return call(() -> { HeaderGroup acceptHeader = new HeaderGroup(); - Stream.of(acceptedResponses).map(ContentType::getMimeType).map(acceptedMimeType -> new BasicHeader(ACCEPT, acceptedMimeType)).forEach(acceptHeader::addHeader); - ClassicHttpRequest request = ClassicRequestBuilder.get(uri).addHeader(acceptHeader.getCondensedHeader(ACCEPT)).build(); + for (ContentType acceptedType : acceptedResponses) { + acceptHeader.addHeader(new BasicHeader(ACCEPT, acceptedType.getMimeType())); + } + + ClassicHttpRequest request = ClassicRequestBuilder.get(absoluteUri) + .addHeader(acceptHeader.getCondensedHeader(ACCEPT)) + .build(); ClassicHttpResponse response = null; try { @@ -206,7 +209,7 @@ public ResponseInputStream getDataStream(URI uri, ContentType ... acceptedRespon } throw e instanceof RuntimeException ? (RuntimeException) e - : new RuntimeException("uri: " + uri + ": " + e.getClass().getSimpleName() + " '" + e.getMessage() + "'", e); + : new RuntimeException(request + ": " + e.getClass().getSimpleName() + " '" + e.getMessage() + "'", e); } }); } @@ -238,25 +241,20 @@ public JobStatusResponse getDirectStatusCha private JobStatusResponse getStatusChange(final Optional sender, final Target target, final Class responseClass) { return call(() -> { + Sender actualSender = getActualSender(sender, globalSender); + URI jobStatusUrl = httpClient.constructUrl(uri -> uri + .appendPath(target.path(actualSender)) + .addParameter(POLLING_QUEUE_QUERY_PARAMETER, actualSender.getPollingQueue().value)); try { - URI uri = new URIBuilder(httpClient.signatureServiceRoot()) - .appendPath(target.path(actualSender)) - .addParameter(POLLING_QUEUE_QUERY_PARAMETER, actualSender.getPollingQueue().value) - .build(); - ClassicHttpRequest get = ClassicRequestBuilder - .get(uri) - .addHeader(ACCEPT, APPLICATION_XML.getMimeType()) - .build(); + ClassicHttpRequest get = ClassicRequestBuilder.get(jobStatusUrl).addHeader(ACCEPT, APPLICATION_XML.getMimeType()).build(); return httpClient.httpClient().execute(get, response -> { StatusCode status = ResponseStatus.resolve(response.getCode()) .throwIf(HttpStatus.SC_TOO_MANY_REQUESTS, s -> new TooEagerPollingException()) .expect(StatusCodeFamily.SUCCESSFUL).orThrow(unexpectedStatus -> exceptionForGeneralError(response)); return new JobStatusResponse<>(status.value() == HttpStatus.SC_NO_CONTENT ? null : Marshalling.unmarshal(response.getEntity().getContent(), responseClass), getNextPermittedPollTime(response)); }); - } catch (URISyntaxException e) { - throw new RuntimeException("Could not create uri, because " + e.getMessage(), e); } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClient.java b/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClient.java index b790850e..618e8836 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClient.java +++ b/src/main/java/no/digipost/signature/client/core/internal/http/SignatureHttpClient.java @@ -1,8 +1,11 @@ package no.digipost.signature.client.core.internal.http; import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.core5.net.URIBuilder; import java.net.URI; +import java.net.URISyntaxException; +import java.util.function.UnaryOperator; public interface SignatureHttpClient { @@ -10,4 +13,16 @@ public interface SignatureHttpClient { HttpClient httpClient(); + default URI constructUrl(UnaryOperator uri) { + URI serviceRoot = signatureServiceRoot(); + URIBuilder uriBuilder = uri.apply(new URIBuilder()); + try { + return uriBuilder.build(); + } catch (URISyntaxException e) { + throw new IllegalStateException( + "Invalid URL constructed for service at " + serviceRoot + ": " + uriBuilder + ". " + + "Reason: " + e.getClass().getSimpleName() + " '" + e.getMessage() + "'", e); + } + } + } From 847632287d7ec0ad912f76dafa591786f4ab0dad Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 16 Feb 2023 13:07:34 +0100 Subject: [PATCH 22/32] Add toString to Sender and PollingQueue --- .../java/no/digipost/signature/client/core/PollingQueue.java | 5 +++++ src/main/java/no/digipost/signature/client/core/Sender.java | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/main/java/no/digipost/signature/client/core/PollingQueue.java b/src/main/java/no/digipost/signature/client/core/PollingQueue.java index 1141cbd9..196aeed3 100644 --- a/src/main/java/no/digipost/signature/client/core/PollingQueue.java +++ b/src/main/java/no/digipost/signature/client/core/PollingQueue.java @@ -30,4 +30,9 @@ public int hashCode() { return Objects.hash(value); } + @Override + public String toString() { + return value != null ? "polling-queue '" + value + "'" : "no specified polling-queue (default)"; + } + } diff --git a/src/main/java/no/digipost/signature/client/core/Sender.java b/src/main/java/no/digipost/signature/client/core/Sender.java index 3bf061ac..684fbfb6 100644 --- a/src/main/java/no/digipost/signature/client/core/Sender.java +++ b/src/main/java/no/digipost/signature/client/core/Sender.java @@ -41,4 +41,9 @@ public int hashCode() { return Objects.hash(organizationNumber, pollingQueue); } + @Override + public String toString() { + return "sender " + organizationNumber + ", " + pollingQueue; + } + } From c49226a2f1bdb695fbaf5af7d14ef8ecfd37c4ae Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 16 Feb 2023 13:08:32 +0100 Subject: [PATCH 23/32] Fix URL handling in ArchiveClient This really needs some further tidying, but this will work for now since the mess is kept inside the internal part of the library. --- .../no/digipost/signature/client/archive/ArchiveClient.java | 3 +-- .../digipost/signature/client/core/internal/ClientHelper.java | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/no/digipost/signature/client/archive/ArchiveClient.java b/src/main/java/no/digipost/signature/client/archive/ArchiveClient.java index ec8c7e89..1f345c9a 100644 --- a/src/main/java/no/digipost/signature/client/archive/ArchiveClient.java +++ b/src/main/java/no/digipost/signature/client/archive/ArchiveClient.java @@ -5,7 +5,6 @@ import no.digipost.signature.client.core.internal.http.HttpIntegrationConfiguration; import no.digipost.signature.client.core.internal.http.SignatureHttpClientFactory; -import java.net.URI; import java.util.Optional; public class ArchiveClient { @@ -17,7 +16,7 @@ public ArchiveClient(HttpIntegrationConfiguration httpIntegrationConfig) { } public ResponseInputStream getPAdES(ArchiveOwner owner, String id) { - return client.getDataStream(URI.create(owner.getOrganizationNumber() + "/archive/documents/" + id + "/pades")); + return client.getDataStream(owner.getOrganizationNumber() + "/archive/documents/" + id + "/pades"); } } diff --git a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java index 5a04492e..af960b2b 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java +++ b/src/main/java/no/digipost/signature/client/core/internal/ClientHelper.java @@ -177,6 +177,10 @@ public XMLDirectSignatureJobStatusResponse sendSignatureJobStatusRequest(final U }); } + public ResponseInputStream getDataStream(String path, ContentType ... acceptedResponses) { + return getDataStream(httpClient.constructUrl(uri -> uri.appendPath(path))); + } + public ResponseInputStream getDataStream(URI absoluteUri, ContentType ... acceptedResponses) { if (!absoluteUri.isAbsolute()) { throw new IllegalArgumentException("'" + absoluteUri + "' is not an absolute URL"); From c0920fc7b12ccb0524571b8d78cbb6c845be0eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20St=C3=B8a?= Date: Thu, 16 Feb 2023 14:29:46 +0100 Subject: [PATCH 24/32] use signature-api-client-java:3.0.0-RC3 --- pom.xml | 2 +- .../signature/client/asice/signature/CreateSignature.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index f5d5def2..dceb88a4 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ 1.8 1.8 set_with_-Dproject.previousVersion=X.Y - remove-spring-dependency-SNAPSHOT + 3.0.0-RC3 1.7.36 diff --git a/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java b/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java index 9a09286d..1d0096d8 100644 --- a/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java +++ b/src/main/java/no/digipost/signature/client/asice/signature/CreateSignature.java @@ -129,8 +129,7 @@ private static Schema createSchema(Collection resources) { .toArray(Source[]::new); SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); - Schema schema = schemaFactory.newSchema(schemaSources); - return schema; + return schemaFactory.newSchema(schemaSources); } catch (Exception e) { throw new RuntimeException("Could not create schema from resources [" + String.join(", ", resources) + "]", e); } From c3af7ec476c441ce3b287b2e21c0847c44fe3aa8 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 16 Feb 2023 14:42:37 +0100 Subject: [PATCH 25/32] Revert to package private field --- src/main/java/no/digipost/signature/client/Certificates.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/no/digipost/signature/client/Certificates.java b/src/main/java/no/digipost/signature/client/Certificates.java index 941895ff..9dc89879 100644 --- a/src/main/java/no/digipost/signature/client/Certificates.java +++ b/src/main/java/no/digipost/signature/client/Certificates.java @@ -35,7 +35,7 @@ public enum Certificates { "prod/commfides_root_ca.cer" ); - public final List certificatePaths; + final List certificatePaths; Certificates(String ... certificatePaths) { this.certificatePaths = Stream.of(certificatePaths) From ea4eab6de7fc1b9470442608c098b55a6a2de90f Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 16 Feb 2023 15:04:40 +0100 Subject: [PATCH 26/32] More information in exception --- .../signature/client/asice/signature/XAdESArtifacts.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/no/digipost/signature/client/asice/signature/XAdESArtifacts.java b/src/main/java/no/digipost/signature/client/asice/signature/XAdESArtifacts.java index c640c4c4..2ec0f40f 100644 --- a/src/main/java/no/digipost/signature/client/asice/signature/XAdESArtifacts.java +++ b/src/main/java/no/digipost/signature/client/asice/signature/XAdESArtifacts.java @@ -27,13 +27,15 @@ final class XAdESArtifacts { public static XAdESArtifacts from(QualifyingProperties qualifyingProperties) { + DOMResult domResult = new DOMResult(); try { - DOMResult domResult = new DOMResult(); marshaller.marshal(qualifyingProperties, domResult); - return from((Document) domResult.getNode()); } catch (JAXBException e) { - throw new RuntimeException(e); + throw new RuntimeException( + "Failed to marshal qualifying properties, because " + + e.getClass().getSimpleName() + " '" + e.getMessage() + "'", e); } + return from((Document) domResult.getNode()); } private static XAdESArtifacts from(Document qualifyingPropertiesDocument) { From e1e9b7657f11a9a79ddef8a8fa7eef72f24dd1de Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 16 Feb 2023 15:22:14 +0100 Subject: [PATCH 27/32] Tidy up response statuscode value classes --- .../client/core/internal/http/ResponseStatus.java | 9 --------- .../client/core/internal/http/StatusCode.java | 13 +++++++------ .../core/internal/http/ResponseStatusTest.java | 13 ------------- .../client/core/internal/http/StatusCodeTest.java | 13 +++++++++++++ 4 files changed, 20 insertions(+), 28 deletions(-) create mode 100644 src/test/java/no/digipost/signature/client/core/internal/http/StatusCodeTest.java diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/ResponseStatus.java b/src/main/java/no/digipost/signature/client/core/internal/http/ResponseStatus.java index 8cf8ed6b..95d9c1f9 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/http/ResponseStatus.java +++ b/src/main/java/no/digipost/signature/client/core/internal/http/ResponseStatus.java @@ -2,7 +2,6 @@ import no.digipost.signature.client.core.exceptions.UnexpectedResponseException; -import java.util.Objects; import java.util.function.BiPredicate; import java.util.function.Function; import java.util.function.Predicate; @@ -72,12 +71,4 @@ public String toString() { return statusCode.toString() + (statusExpectation.test(statusCode) ? "" : " (unexpected)"); } - @Override - public boolean equals(Object obj) { - if (obj instanceof ResponseStatus) { - return Objects.equals(this.statusCode, ((ResponseStatus) obj).statusCode); - } - return false; - } - } diff --git a/src/main/java/no/digipost/signature/client/core/internal/http/StatusCode.java b/src/main/java/no/digipost/signature/client/core/internal/http/StatusCode.java index ae804545..d7d4409c 100644 --- a/src/main/java/no/digipost/signature/client/core/internal/http/StatusCode.java +++ b/src/main/java/no/digipost/signature/client/core/internal/http/StatusCode.java @@ -1,10 +1,9 @@ package no.digipost.signature.client.core.internal.http; import java.util.EnumSet; -import java.util.Objects; import java.util.Set; -public class StatusCode { +public final class StatusCode { private final int value; @@ -43,10 +42,12 @@ public String toString() { @Override public boolean equals(Object obj) { - if (obj instanceof StatusCode) { - return Objects.equals(this.value, ((StatusCode) obj).value); - } - return false; + return obj instanceof StatusCode && this.value == ((StatusCode) obj).value; + } + + @Override + public int hashCode() { + return Integer.hashCode(value); } } diff --git a/src/test/java/no/digipost/signature/client/core/internal/http/ResponseStatusTest.java b/src/test/java/no/digipost/signature/client/core/internal/http/ResponseStatusTest.java index da92e18a..9cc2368b 100644 --- a/src/test/java/no/digipost/signature/client/core/internal/http/ResponseStatusTest.java +++ b/src/test/java/no/digipost/signature/client/core/internal/http/ResponseStatusTest.java @@ -4,9 +4,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.quicktheories.QuickTheory.qt; -import static org.quicktheories.generators.SourceDSL.integers; public class ResponseStatusTest { @@ -24,14 +21,4 @@ public void resolveClientError() { assertThat(ResponseStatus.resolve(422).get().family(), is(StatusCodeFamily.CLIENT_ERROR)); } - @Test - public void correctEqualsHashCodeForAnyResolvedStatus() { - qt() - .forAll(integers().between(0, 1000)) - .checkAssert(anyStatusCode -> { - assertThat(ResponseStatus.resolve(anyStatusCode).get(), is(ResponseStatus.resolve(anyStatusCode).get())); - assertThat(ResponseStatus.resolve(anyStatusCode).get(), not(ResponseStatus.resolve(anyStatusCode + 1).get())); - }); - } - } diff --git a/src/test/java/no/digipost/signature/client/core/internal/http/StatusCodeTest.java b/src/test/java/no/digipost/signature/client/core/internal/http/StatusCodeTest.java new file mode 100644 index 00000000..d8948081 --- /dev/null +++ b/src/test/java/no/digipost/signature/client/core/internal/http/StatusCodeTest.java @@ -0,0 +1,13 @@ +package no.digipost.signature.client.core.internal.http; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +class StatusCodeTest { + + @Test + void correctEqualsAndHashCode() { + EqualsVerifier.forClass(StatusCode.class).verify(); + } + +} From 82d4fd65975b123f299d77deffc5ad2c10b6cc7b Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 16 Feb 2023 15:29:11 +0100 Subject: [PATCH 28/32] Improve exception message --- .../no/digipost/signature/client/direct/StatusReference.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/no/digipost/signature/client/direct/StatusReference.java b/src/main/java/no/digipost/signature/client/direct/StatusReference.java index b2cd1ca8..4574432e 100644 --- a/src/main/java/no/digipost/signature/client/direct/StatusReference.java +++ b/src/main/java/no/digipost/signature/client/direct/StatusReference.java @@ -55,10 +55,11 @@ private StatusReference(URI statusUrl, String statusQueryToken) { } public URI getStatusUrl() { + URIBuilder uriBuilder = new URIBuilder(statusUrl).addParameter(STATUS_QUERY_TOKEN_PARAM_NAME, statusQueryToken); try { - return new URIBuilder(statusUrl).addParameter(STATUS_QUERY_TOKEN_PARAM_NAME, statusQueryToken).build(); + return uriBuilder.build(); } catch (URISyntaxException e) { - throw new RuntimeException(e); + throw new IllegalStateException("Unable to create valid URL from " + uriBuilder, e); } } From fe38d751b497e9621ed9f879ebbc1ffa3f46e9ca Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 16 Feb 2023 15:34:22 +0100 Subject: [PATCH 29/32] Upgrade dependencies Remove jcl-over-slf4j (Java Commons Logging over SLF4J), as we have no dependencies requiring commons-logging, and do not need this replacement. --- pom.xml | 18 ++++++------------ .../signature/client/ClientConfiguration.java | 9 ++++++++- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/pom.xml b/pom.xml index dceb88a4..581700a9 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ org.junit junit-bom - 5.9.0-M1 + 5.9.2 pom import @@ -48,7 +48,7 @@ no.digipost certificate-validator - 2.5 + 2.6 org.bouncycastle @@ -60,12 +60,12 @@ org.apache.httpcomponents.client5 httpclient5 - 5.1.3 + 5.2.1 org.apache.httpcomponents.core5 httpcore5 - 5.1.3 + 5.2.1 @@ -76,7 +76,7 @@ org.glassfish.jaxb jaxb-runtime - 2.3.6 + 2.3.8 runtime @@ -102,12 +102,6 @@ slf4j-api ${slf4j.version} - - org.slf4j - jcl-over-slf4j - ${slf4j.version} - runtime - org.slf4j slf4j-simple @@ -134,7 +128,7 @@ nl.jqno.equalsverifier equalsverifier - 3.12.4 + 3.13.2 test diff --git a/src/main/java/no/digipost/signature/client/ClientConfiguration.java b/src/main/java/no/digipost/signature/client/ClientConfiguration.java index ede182bc..57a0d078 100644 --- a/src/main/java/no/digipost/signature/client/ClientConfiguration.java +++ b/src/main/java/no/digipost/signature/client/ClientConfiguration.java @@ -156,6 +156,8 @@ public static class Builder { private Duration socketTimeout = DEFAULT_SOCKET_TIMEOUT; private Duration connectTimeout = DEFAULT_CONNECT_TIMEOUT; + private Duration connectionRequestTimeout = connectTimeout; + private Duration responseArrivalTimeout = connectTimeout; private Optional customUserAgentPart = Optional.empty(); private URI serviceRoot = ServiceUri.PRODUCTION.uri; @@ -374,7 +376,12 @@ public ClientConfiguration build() { .setSslContext(sslContext()) .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) .build()); - RequestConfig.Builder requestConfigBuilder = RequestConfig.custom().setConnectionRequestTimeout(Timeout.ofMilliseconds(connectTimeout.toMillis())); + + RequestConfig.Builder requestConfigBuilder = RequestConfig.custom() + .setConnectionRequestTimeout(Timeout.ofMilliseconds(connectionRequestTimeout.toMillis())) + .setConnectTimeout(Timeout.ofMilliseconds(connectTimeout.toMillis())) + .setResponseTimeout(Timeout.ofMilliseconds(responseArrivalTimeout.toMillis())); + httpClientBuilder .setConnectionManager(poolingHttpClientConnectionManager.build()) .setDefaultRequestConfig(requestConfigBuilder.build()) From a3ec5297977ef57d0e6f5716b896984f9fc1963a Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 16 Feb 2023 15:41:34 +0100 Subject: [PATCH 30/32] Upgrade Maven plugins --- pom.xml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index 581700a9..5bd32744 100644 --- a/pom.xml +++ b/pom.xml @@ -255,11 +255,11 @@ maven-surefire-plugin - 3.0.0-M7 + 3.0.0-M9 maven-deploy-plugin - 3.0.0 + 3.1.0 maven-clean-plugin @@ -287,7 +287,7 @@ maven-enforcer-plugin - 3.1.0 + 3.2.1 @@ -299,11 +299,7 @@ true - - 3.3.1 + 3.6.3 @@ -311,7 +307,7 @@ org.codehaus.mojo versions-maven-plugin - 2.11.0 + 2.14.2 From cba9548615842096fe80f325a5733fed5abbf9ba Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 16 Feb 2023 15:52:11 +0100 Subject: [PATCH 31/32] Upgrade maven-publish GitHub action To support newer Maven version. --- .github/workflows/build.yml | 6 +++--- pom.xml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15f4b61b..d1d69a8f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: distribution: temurin cache: "maven" - name: Build with Maven - run: mvn --settings .mvn/settings.xml -B package --no-transfer-progress --file pom.xml + run: mvn --settings .mvn/settings.xml -B package --no-transfer-progress makeversion: if: github.ref != 'refs/heads/main' @@ -47,7 +47,7 @@ jobs: name: Deploy snapshot steps: - uses: actions/checkout@v3 - - uses: digipost/action-maven-publish@1.2.0 + - uses: digipost/action-maven-publish@1.3.2 with: sonatype_secrets: ${{ secrets.sonatype_secrets }} release_version: ${{ needs.makeversion.outputs.version }} @@ -62,7 +62,7 @@ jobs: - name: Check out Git repository uses: actions/checkout@v3 - name: Release to Central Repository - uses: digipost/action-maven-publish@1.2.0 + uses: digipost/action-maven-publish@1.3.2 with: sonatype_secrets: ${{ secrets.sonatype_secrets }} release_version: ${{ needs.makeversion.outputs.version }} diff --git a/pom.xml b/pom.xml index 5bd32744..ded32f4c 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ no.digipost digipost-open-super-pom - 7 + 8 From 9f3e1deada779ceccdf730129918c6b2025420f4 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 16 Feb 2023 16:00:36 +0100 Subject: [PATCH 32/32] Fix javadoc heading levels --- .../java/no/digipost/signature/client/ClientConfiguration.java | 2 +- .../signature/client/portal/PortalJobStatusChanged.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/no/digipost/signature/client/ClientConfiguration.java b/src/main/java/no/digipost/signature/client/ClientConfiguration.java index 57a0d078..ba125e43 100644 --- a/src/main/java/no/digipost/signature/client/ClientConfiguration.java +++ b/src/main/java/no/digipost/signature/client/ClientConfiguration.java @@ -298,7 +298,7 @@ public Builder enableDocumentBundleDiskDump(Path directory) { * together with the {@link SignatureJob job} it was created for. The processor is not responsible for closing * the stream, as this is handled by the library itself. * - *

A note on performance

+ *

A note on performance: * The processor is free to do what it want with the passed stream, but bear in mind that the time * used by a processor adds to the processing time to create signature jobs. * diff --git a/src/main/java/no/digipost/signature/client/portal/PortalJobStatusChanged.java b/src/main/java/no/digipost/signature/client/portal/PortalJobStatusChanged.java index 90d6c674..4552ab00 100644 --- a/src/main/java/no/digipost/signature/client/portal/PortalJobStatusChanged.java +++ b/src/main/java/no/digipost/signature/client/portal/PortalJobStatusChanged.java @@ -16,7 +16,7 @@ * Indicates a job which has got a new {@link PortalJobStatus status} * since the last time its status was queried. * - *

Confirmation

+ *

Confirmation

* * When the client {@link Confirmable confirms} this, the job and its associated * resources will become unavailable through the Signature API.