Skip to content

Commit 37be46d

Browse files
authored
Add equals/hashcode for Destination (#341)
1 parent e03d80f commit 37be46d

File tree

5 files changed

+356
-15
lines changed

5 files changed

+356
-15
lines changed

cloudplatform/cloudplatform-connectivity/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/DefaultHttpDestination.java

+37-12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
package com.sap.cloud.sdk.cloudplatform.connectivity;
66

7+
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationKeyStoreComparator.resolveCertificatesOnly;
8+
import static com.sap.cloud.sdk.cloudplatform.connectivity.DestinationKeyStoreComparator.resolveKeyStoreHashCode;
9+
710
import java.net.URI;
811
import java.security.KeyStore;
912
import java.util.ArrayList;
@@ -20,6 +23,9 @@
2023
import javax.annotation.Nullable;
2124
import javax.net.ssl.SSLContext;
2225

26+
import org.apache.commons.lang3.builder.EqualsBuilder;
27+
import org.apache.commons.lang3.builder.HashCodeBuilder;
28+
2329
import com.google.common.collect.ImmutableList;
2430
import com.google.common.collect.Lists;
2531
import com.google.common.net.HttpHeaders;
@@ -36,36 +42,30 @@
3642
import io.vavr.control.Option;
3743
import io.vavr.control.Try;
3844
import lombok.AccessLevel;
39-
import lombok.EqualsAndHashCode;
4045
import lombok.Getter;
4146
import lombok.experimental.Delegate;
4247
import lombok.extern.slf4j.Slf4j;
4348

4449
/**
4550
* Immutable default implementation of the {@link HttpDestination} interface.
4651
*/
47-
@EqualsAndHashCode
4852
@Slf4j
4953
public final class DefaultHttpDestination implements HttpDestination
5054
{
5155
@Delegate
5256
private final DestinationProperties baseProperties;
5357

54-
@EqualsAndHashCode.Exclude
5558
private final KeyStore keyStore;
56-
@EqualsAndHashCode.Exclude
5759
private final KeyStore trustStore;
5860

5961
@Nonnull
6062
final ImmutableList<Header> customHeaders;
6163

6264
@Nonnull
6365
@Getter( AccessLevel.PACKAGE )
64-
@EqualsAndHashCode.Exclude
6566
private final ImmutableList<DestinationHeaderProvider> customHeaderProviders;
6667

6768
@Nonnull
68-
@EqualsAndHashCode.Exclude
6969
private final ImmutableList<DestinationHeaderProvider> headerProvidersFromClassLoading;
7070

7171
// the following 'cached' fields are ALWAYS derived from the baseProperties and stored in the corresponding fields
@@ -77,27 +77,21 @@ public final class DefaultHttpDestination implements HttpDestination
7777
// furthermore, it is safe to exclude these fields from the equals and hashCode methods because their values are
7878
// purely derived from the baseProperties, which are included in the equals and hashCode methods.
7979
@Nonnull
80-
@EqualsAndHashCode.Exclude
8180
private final Option<ProxyConfiguration> cachedProxyConfiguration;
8281

8382
@Nonnull
84-
@EqualsAndHashCode.Exclude
8583
private final Option<ProxyType> cachedProxyType;
8684

8785
@Nonnull
88-
@EqualsAndHashCode.Exclude
8986
private final Option<BasicCredentials> cachedBasicCredentials;
9087

9188
@Nonnull
92-
@EqualsAndHashCode.Exclude
9389
private final AuthenticationType cachedAuthenticationType;
9490

9591
@Nonnull
96-
@EqualsAndHashCode.Exclude
9792
private final ImmutableList<Header> cachedHeadersFromProperties;
9893

9994
@Nonnull
100-
@EqualsAndHashCode.Exclude
10195
private final ImmutableList<Header> cachedProxyAuthorizationHeaders;
10296

10397
private DefaultHttpDestination(
@@ -511,6 +505,37 @@ public static Builder fromDestination( @Nonnull final Destination destination )
511505
return builder;
512506
}
513507

508+
@Override
509+
public boolean equals( @Nullable final Object o )
510+
{
511+
if( this == o ) {
512+
return true;
513+
}
514+
515+
if( o == null || getClass() != o.getClass() ) {
516+
return false;
517+
}
518+
519+
final DefaultHttpDestination that = (DefaultHttpDestination) o;
520+
return new EqualsBuilder()
521+
.append(baseProperties, that.baseProperties)
522+
.append(customHeaders, that.customHeaders)
523+
.append(resolveCertificatesOnly(keyStore), resolveCertificatesOnly(that.keyStore))
524+
.append(resolveCertificatesOnly(trustStore), resolveCertificatesOnly(that.trustStore))
525+
.isEquals();
526+
}
527+
528+
@Override
529+
public int hashCode()
530+
{
531+
return new HashCodeBuilder(17, 37)
532+
.append(baseProperties)
533+
.append(customHeaders)
534+
.append(resolveKeyStoreHashCode(keyStore))
535+
.append(resolveKeyStoreHashCode(trustStore))
536+
.toHashCode();
537+
}
538+
514539
/**
515540
* Builder class to allow for easy creation of an immutable {@code DefaultHttpDestination} instance.
516541
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright (c) 2024 SAP SE or an SAP affiliate company. All rights reserved.
3+
*/
4+
5+
package com.sap.cloud.sdk.cloudplatform.connectivity;
6+
7+
import java.security.KeyStore;
8+
import java.security.cert.Certificate;
9+
import java.util.ArrayList;
10+
import java.util.Enumeration;
11+
12+
import javax.annotation.Nonnull;
13+
14+
import org.apache.commons.lang3.builder.HashCodeBuilder;
15+
16+
import lombok.extern.slf4j.Slf4j;
17+
18+
@Slf4j
19+
class DestinationKeyStoreComparator
20+
{
21+
static int INITIAL_HASH_CODE = 17;
22+
23+
/**
24+
* Calculate the SAP Cloud SDK compatible KeyStore hash code. The method may return a static number
25+
* {@link DestinationKeyStoreComparator#INITIAL_HASH_CODE} in case the key-store was not initialized or contains
26+
* non-certificate based elements.
27+
*
28+
* @param ks
29+
* The KeyStore to calculate the hash code for.
30+
* @return The key-store hash code dynamically computed on behalf of stored certificates.
31+
*/
32+
static int resolveKeyStoreHashCode( @Nonnull final KeyStore ks )
33+
{
34+
final HashCodeBuilder out = new HashCodeBuilder(INITIAL_HASH_CODE, 37);
35+
final Certificate[] certificates = resolveCertificatesOnly(ks);
36+
out.append(certificates);
37+
return out.toHashCode();
38+
}
39+
40+
/**
41+
* Resolve certificates-only from a KeyStore.
42+
*
43+
* @param ks
44+
* The KeyStore to iterate.
45+
* @return An array with certificates, or empty in case of error or non-certificate based keystore entries.
46+
*/
47+
@Nonnull
48+
static Certificate[] resolveCertificatesOnly( @Nonnull final KeyStore ks )
49+
{
50+
final ArrayList<Certificate> out = new ArrayList<>();
51+
try {
52+
final Enumeration<String> aliases = ks.aliases();
53+
while( aliases.hasMoreElements() ) {
54+
final String alias = aliases.nextElement();
55+
if( ks.getCertificate(alias) == null ) {
56+
return new Certificate[0];
57+
}
58+
out.add(ks.getCertificate(alias));
59+
}
60+
}
61+
catch( final Exception e ) {
62+
log.debug("Error while resolving certificates from KeyStore", e);
63+
return new Certificate[0];
64+
}
65+
return out.toArray(new Certificate[0]);
66+
}
67+
}

cloudplatform/cloudplatform-connectivity/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/DefaultHttpDestinationTest.java

+33
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
import java.net.URI;
1212
import java.net.URISyntaxException;
13+
import java.security.KeyPair;
1314
import java.security.KeyStore;
15+
import java.security.cert.Certificate;
1416
import java.util.Collection;
1517
import java.util.Collections;
1618
import java.util.List;
@@ -139,6 +141,37 @@ void testEqualsIsImplemented()
139141
assertThat(firstDestination).isEqualTo(secondDestination).isNotSameAs(secondDestination);
140142
}
141143

144+
@SneakyThrows
145+
@Test
146+
void testEqualsWithKeyStore()
147+
{
148+
final KeyPair keyPair = DestinationKeyStoreComparatorTest.generateKeyPair();
149+
final Certificate cert = DestinationKeyStoreComparatorTest.generateCertificate(keyPair, "a");
150+
151+
final KeyStore keystore1 = KeyStore.getInstance("JKS");
152+
keystore1.load(null);
153+
keystore1.setKeyEntry("a", keyPair.getPrivate(), new char[0], new Certificate[] { cert });
154+
155+
final KeyStore keystore2 = KeyStore.getInstance("JKS");
156+
keystore2.load(null);
157+
keystore2.setKeyEntry("a", keyPair.getPrivate(), new char[0], new Certificate[] { cert });
158+
159+
// check for destinations with comparable key-stores
160+
final DefaultHttpDestination dest1 = DefaultHttpDestination.builder(VALID_URI).keyStore(keystore1).build();
161+
final DefaultHttpDestination dest2 = DefaultHttpDestination.builder(VALID_URI).keyStore(keystore2).build();
162+
163+
assertThat(dest1).isEqualTo(dest2);
164+
assertThat(dest1).hasSameHashCodeAs(dest2);
165+
166+
// check for destination with empty key-store
167+
final KeyStore keystore3 = KeyStore.getInstance("JKS");
168+
keystore3.load(null);
169+
170+
final DefaultHttpDestination dest3 = DefaultHttpDestination.builder(VALID_URI).keyStore(keystore3).build();
171+
assertThat(dest1).isNotEqualTo(dest3);
172+
assertThat(dest1).doesNotHaveSameHashCodeAs(dest3);
173+
}
174+
142175
@Test
143176
void testHashCodeIsImplemented()
144177
{

0 commit comments

Comments
 (0)