Skip to content

Commit 97e5ee7

Browse files
angelmp01EDPCommunity Automated Test
authored andcommitted
Feature/edpc 4280 bitbucket service project existance (#6)
Co-authored-by: EDPCommunity Automated Test <x2odsedpcomm@boehringer-ingelheim.com>
1 parent a52054f commit 97e5ee7

9 files changed

Lines changed: 838 additions & 91 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -1,188 +1,229 @@
11
package org.opendevstack.apiservice.externalservice.bitbucket.client;
22

3+
import lombok.extern.slf4j.Slf4j;
34
import org.opendevstack.apiservice.externalservice.bitbucket.config.BitbucketServiceConfiguration;
45
import org.opendevstack.apiservice.externalservice.bitbucket.config.BitbucketServiceConfiguration.BitbucketInstanceConfig;
56
import org.opendevstack.apiservice.externalservice.bitbucket.exception.BitbucketException;
6-
import lombok.extern.slf4j.Slf4j;
77
import org.springframework.boot.web.client.RestTemplateBuilder;
8+
import org.springframework.cache.annotation.CacheEvict;
9+
import org.springframework.cache.annotation.Cacheable;
810
import org.springframework.http.client.SimpleClientHttpRequestFactory;
911
import org.springframework.stereotype.Component;
1012
import org.springframework.web.client.RestTemplate;
1113

12-
import javax.net.ssl.*;
14+
import javax.net.ssl.HttpsURLConnection;
15+
import javax.net.ssl.SSLContext;
16+
import javax.net.ssl.TrustManager;
17+
import javax.net.ssl.X509TrustManager;
1318
import java.security.KeyManagementException;
1419
import java.security.NoSuchAlgorithmException;
1520
import java.security.cert.X509Certificate;
1621
import java.util.Map;
17-
import java.util.concurrent.ConcurrentHashMap;
22+
import java.util.Set;
1823

1924
/**
20-
* Factory for creating BitbucketApiClient instances.
25+
* Factory for creating {@link BitbucketApiClient} instances.
2126
* Uses the factory pattern to provide configured clients for different Bitbucket instances.
2227
* Clients are cached and reused for efficiency.
2328
*/
2429
@Component
2530
@Slf4j
2631
public class BitbucketApiClientFactory {
27-
32+
2833
private final BitbucketServiceConfiguration configuration;
29-
private final Map<String, BitbucketApiClient> clientCache;
3034
private final RestTemplateBuilder restTemplateBuilder;
31-
35+
3236
/**
33-
* Constructor with dependency injection
34-
*
35-
* @param configuration Bitbucket service configuration
37+
* Constructor with dependency injection.
38+
*
39+
* @param configuration Bitbucket service configuration
3640
* @param restTemplateBuilder RestTemplate builder for creating HTTP clients
3741
*/
38-
public BitbucketApiClientFactory(BitbucketServiceConfiguration configuration,
42+
public BitbucketApiClientFactory(BitbucketServiceConfiguration configuration,
3943
RestTemplateBuilder restTemplateBuilder) {
4044
this.configuration = configuration;
4145
this.restTemplateBuilder = restTemplateBuilder;
42-
this.clientCache = new ConcurrentHashMap<>();
43-
44-
log.info("BitbucketApiClientFactory initialized with {} instance(s)",
46+
47+
log.info("BitbucketApiClientFactory initialized with {} instance(s)",
4548
configuration.getInstances().size());
4649
}
47-
50+
4851
/**
49-
* Get a BitbucketApiClient for a specific instance
50-
*
52+
* Resolve the effective instance name.
53+
* <ul>
54+
* <li>If the default instance is configured via {@code externalservices.bitbucket.default-instance}, it is returned.</li>
55+
* <li>Otherwise the first entry of the instances map is returned (insertion order).</li>
56+
* <li>If no instances are configured at all, a {@link BitbucketException} is thrown.</li>
57+
* </ul>
58+
*
59+
* @return The resolved default instance name (never {@code null}/blank)
60+
* @throws BitbucketException if no Bitbucket instances are configured
61+
*/
62+
public String getDefaultInstanceName() throws BitbucketException {
63+
64+
String defaultInstance = configuration.getDefaultInstance();
65+
if (defaultInstance != null && !defaultInstance.isBlank()) {
66+
return defaultInstance;
67+
}
68+
69+
Map<String, ?> instances = configuration.getInstances();
70+
if (instances == null || instances.isEmpty()) {
71+
throw new BitbucketException("No Bitbucket instances configured");
72+
}
73+
74+
return instances.keySet().iterator().next();
75+
}
76+
77+
/**
78+
* Get a {@link BitbucketApiClient} for a specific instance.
79+
* If {@code instanceName} is {@code null} or blank, a {@link BitbucketException} is thrown.
80+
*
5181
* @param instanceName Name of the Bitbucket instance
5282
* @return Configured BitbucketApiClient
53-
* @throws BitbucketException if the instance is not configured
83+
* @throws BitbucketException if the instance name is null/blank or not configured
5484
*/
85+
@Cacheable(value = "bitbucketApiClients", key = "#instanceName",
86+
condition = "#instanceName != null && !#instanceName.isBlank()")
5587
public BitbucketApiClient getClient(String instanceName) throws BitbucketException {
56-
// Check cache first
57-
if (clientCache.containsKey(instanceName)) {
58-
log.debug("Returning cached client for instance '{}'", instanceName);
59-
return clientCache.get(instanceName);
88+
if (instanceName == null || instanceName.isBlank()) {
89+
throw new BitbucketException(
90+
String.format("Provide instance name. Available instances: %s",
91+
configuration.getInstances().keySet()));
6092
}
61-
62-
// Create new client
93+
6394
BitbucketInstanceConfig instanceConfig = configuration.getInstances().get(instanceName);
64-
95+
6596
if (instanceConfig == null) {
6697
throw new BitbucketException(
67-
String.format("Bitbucket instance '%s' is not configured. Available instances: %s",
68-
instanceName, configuration.getInstances().keySet())
69-
);
98+
String.format("Bitbucket instance '%s' is not configured. Available instances: %s",
99+
instanceName, configuration.getInstances().keySet()));
70100
}
71-
101+
72102
log.info("Creating new BitbucketApiClient for instance '{}'", instanceName);
73-
103+
74104
RestTemplate restTemplate = createRestTemplate(instanceConfig);
75-
BitbucketApiClient client = new BitbucketApiClient(instanceName, instanceConfig, restTemplate);
76-
77-
// Cache the client
78-
clientCache.put(instanceName, client);
79-
80-
return client;
105+
return new BitbucketApiClient(instanceName, instanceConfig, restTemplate);
81106
}
82-
107+
83108
/**
84-
* Get the default client (first configured instance)
85-
*
86-
* @return BitbucketApiClient for the first configured instance
109+
* Get the default client, as determined by {@code externalservices.bitbucket.default-instance}.
110+
* Falls back to the first configured instance when {@code default-instance} is not set.
111+
*
112+
* @return BitbucketApiClient for the default instance
87113
* @throws BitbucketException if no instances are configured
88114
*/
89-
public BitbucketApiClient getDefaultClient() throws BitbucketException {
90-
if (configuration.getInstances().isEmpty()) {
91-
throw new BitbucketException("No Bitbucket instances configured");
92-
}
93-
94-
String firstInstanceName = configuration.getInstances().keySet().iterator().next();
95-
log.debug("Using default instance: '{}'", firstInstanceName);
96-
97-
return getClient(firstInstanceName);
115+
@Cacheable(value = "bitbucketApiClients", key = "'default'")
116+
public BitbucketApiClient getClient() throws BitbucketException {
117+
String defaultInstanceName = getDefaultInstanceName();
118+
BitbucketInstanceConfig instanceConfig = configuration.getInstances().get(defaultInstanceName);
119+
RestTemplate restTemplate = createRestTemplate(instanceConfig);
120+
121+
return new BitbucketApiClient(defaultInstanceName, instanceConfig, restTemplate);
98122
}
99-
123+
100124
/**
101-
* Get all available instance names
102-
*
125+
* Get all available instance names.
126+
*
103127
* @return Set of configured instance names
104128
*/
105-
public java.util.Set<String> getAvailableInstances() {
129+
public Set<String> getAvailableInstances() {
106130
return configuration.getInstances().keySet();
107131
}
108-
132+
109133
/**
110-
* Check if an instance is configured
111-
*
134+
* Check if an instance is configured.
135+
*
112136
* @param instanceName Name of the instance to check
113137
* @return true if configured, false otherwise
114138
*/
115139
public boolean hasInstance(String instanceName) {
116140
return configuration.getInstances().containsKey(instanceName);
117141
}
118-
142+
119143
/**
120-
* Clear the client cache (useful for testing or when configuration changes)
144+
* Clear the client cache (useful for testing or when configuration changes).
121145
*/
146+
@CacheEvict(value = "bitbucketApiClients", allEntries = true)
122147
public void clearCache() {
123148
log.info("Clearing BitbucketApiClient cache");
124-
clientCache.clear();
125149
}
126150

127151
/**
128-
* Create a configured RestTemplate for a Bitbucket instance
129-
*
152+
* Create a configured RestTemplate for a Bitbucket instance.
153+
*
130154
* @param config Configuration for the instance
131155
* @return Configured RestTemplate
132156
*/
133157
private RestTemplate createRestTemplate(BitbucketInstanceConfig config) {
134158
RestTemplate restTemplate = restTemplateBuilder.build();
135159

136-
// Set timeouts using SimpleClientHttpRequestFactory
137-
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
138-
requestFactory.setConnectTimeout(config.getConnectionTimeout());
139-
requestFactory.setReadTimeout(config.getReadTimeout());
140-
restTemplate.setRequestFactory(requestFactory);
141-
142-
// Configure SSL if trustAllCertificates is enabled
143160
if (config.isTrustAllCertificates()) {
144161
log.warn("Trust all certificates is enabled for Bitbucket connection. " +
145162
"This should only be used in development environments!");
146-
configureTrustAllCertificates(restTemplate);
163+
restTemplate.setRequestFactory(createTrustAllRequestFactory(config));
164+
} else {
165+
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
166+
requestFactory.setConnectTimeout(config.getConnectionTimeout());
167+
requestFactory.setReadTimeout(config.getReadTimeout());
168+
restTemplate.setRequestFactory(requestFactory);
147169
}
148170

149171
return restTemplate;
150172
}
151-
173+
152174
/**
153-
* Configure RestTemplate to trust all SSL certificates
154-
* WARNING: This should only be used in development environments
155-
*
156-
* @param restTemplate RestTemplate to configure
175+
* Create a {@link SimpleClientHttpRequestFactory} that trusts all SSL certificates
176+
* <b>only for this specific RestTemplate</b>, without modifying the JVM-wide defaults.
177+
* <p>
178+
* WARNING: This should only be used in development environments.
179+
*
180+
* @param config Instance configuration (for timeouts)
181+
* @return A request factory whose connections skip SSL verification
157182
*/
158183
@SuppressWarnings({"java:S4830", "java:S1186"}) // Intentionally disabling SSL validation for development
159-
private void configureTrustAllCertificates(RestTemplate restTemplate) {
184+
private SimpleClientHttpRequestFactory createTrustAllRequestFactory(BitbucketInstanceConfig config) {
160185
try {
161186
TrustManager[] trustAllCerts = new TrustManager[]{
162187
new X509TrustManager() {
163188
public X509Certificate[] getAcceptedIssuers() {
164189
return new X509Certificate[0];
165190
}
166-
// Intentionally empty - trusting all certificates for development environments
167191
public void checkClientTrusted(X509Certificate[] certs, String authType) {
168192
// No validation performed - development only
169193
}
170-
// Intentionally empty - trusting all certificates for development environments
171194
public void checkServerTrusted(X509Certificate[] certs, String authType) {
172195
// No validation performed - development only
173196
}
174197
}
175198
};
176-
199+
177200
SSLContext sslContext = SSLContext.getInstance("TLS");
178201
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
179-
180-
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
181-
// Intentionally disabling hostname verification for development environments
182-
HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
183-
202+
203+
final javax.net.ssl.SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
204+
final javax.net.ssl.HostnameVerifier trustAllHostnames = (hostname, session) -> true;
205+
206+
// Override prepareConnection so SSL settings apply only to this RestTemplate
207+
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory() {
208+
@Override
209+
protected void prepareConnection(java.net.HttpURLConnection connection, String httpMethod) throws java.io.IOException {
210+
if (connection instanceof HttpsURLConnection httpsConnection) {
211+
httpsConnection.setSSLSocketFactory(sslSocketFactory);
212+
httpsConnection.setHostnameVerifier(trustAllHostnames);
213+
}
214+
super.prepareConnection(connection, httpMethod);
215+
}
216+
};
217+
requestFactory.setConnectTimeout(config.getConnectionTimeout());
218+
requestFactory.setReadTimeout(config.getReadTimeout());
219+
return requestFactory;
220+
184221
} catch (NoSuchAlgorithmException | KeyManagementException e) {
185-
log.error("Failed to configure SSL trust all certificates", e);
222+
log.error("Failed to configure SSL trust all certificates, falling back to default factory", e);
223+
SimpleClientHttpRequestFactory fallback = new SimpleClientHttpRequestFactory();
224+
fallback.setConnectTimeout(config.getConnectionTimeout());
225+
fallback.setReadTimeout(config.getReadTimeout());
226+
return fallback;
186227
}
187228
}
188229
}

external-service-bitbucket/src/main/java/org/opendevstack/apiservice/externalservice/bitbucket/config/BitbucketServiceConfiguration.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,25 @@
1616
@Data
1717
public class BitbucketServiceConfiguration {
1818

19+
/**
20+
* Optional name of the default Bitbucket instance.
21+
* When set, {@code BitbucketApiClientFactory#getClient()} will use this instance.
22+
* If not set, the first entry in the instances map is used as default.
23+
*/
24+
private String defaultInstance;
25+
1926
/**
2027
* Map of Bitbucket instances with instance name as key and configuration as value.
2128
* Example:
2229
* externalservice:
2330
* bitbucket:
2431
* instances:
2532
* dev:
26-
* base-url: https://bitbucket.dev.example.com
33+
* base-url: "https://bitbucket.dev.example.com"
2734
* username: admin
2835
* password: password123
2936
* prod:
30-
* base-url: https://bitbucket.example.com
37+
* base-url: "https://bitbucket.example.com"
3138
* username: admin
3239
* password: secret
3340
*/
@@ -39,7 +46,7 @@ public class BitbucketServiceConfiguration {
3946
@Data
4047
public static class BitbucketInstanceConfig {
4148
/**
42-
* The base URL of the Bitbucket server (e.g., https://bitbucket.example.com)
49+
* The base URL of the Bitbucket server (e.g., "https://bitbucket.example.com")
4350
*/
4451
private String baseUrl;
4552

external-service-bitbucket/src/main/java/org/opendevstack/apiservice/externalservice/bitbucket/service/BitbucketService.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ public interface BitbucketService extends ExternalService {
3434
*/
3535
boolean branchExists(String instanceName, String projectKey, String repositorySlug, String branchName) throws BitbucketException;
3636

37+
/**
38+
* Check if a project exists in a specific Bitbucket instance.
39+
*
40+
* @param instanceName Name of the Bitbucket instance
41+
* @param projectKey Project key (e.g., "PROJ")
42+
* @return true if the project exists, false if it does not exist
43+
* @throws BitbucketException if the check fails due to a non-functional error
44+
* (e.g., Bitbucket unreachable, bad credentials, network errors).
45+
* A non-existent project is NOT surfaced as an exception — it returns false.
46+
*/
47+
boolean projectExists(String instanceName, String projectKey) throws BitbucketException;
48+
3749
/**
3850
* Get all available Bitbucket instance names
3951
*

0 commit comments

Comments
 (0)