From 940611e3741c4dbb8a99b37371412b1382833919 Mon Sep 17 00:00:00 2001 From: Ioannis Chatzis <ioannis.chatzis@upatras.gr> Date: Wed, 16 Oct 2024 18:41:25 +0300 Subject: [PATCH] Implementation Commit --- pom.xml | 101 +++++ .../osl/etsi/util/KeycloakAuthenticator.java | 175 ++++++++ .../util/ServiceSpecificationFetcher.java | 415 ++++++++++++++++++ src/main/resources/Dockerfile | 17 + src/main/resources/config.properties | 7 + src/main/resources/logback.xml | 20 + 6 files changed, 735 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/org/osl/etsi/util/KeycloakAuthenticator.java create mode 100644 src/main/java/org/osl/etsi/util/ServiceSpecificationFetcher.java create mode 100644 src/main/resources/Dockerfile create mode 100644 src/main/resources/config.properties create mode 100644 src/main/resources/logback.xml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a76a390 --- /dev/null +++ b/pom.xml @@ -0,0 +1,101 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>org.etsi.osl.util</groupId> + <artifactId>servicespecificationfetcher</artifactId> + <version>0.0.1-SNAPSHOT</version> + + <!-- Specify the Java version you're using --> + <properties> + <maven.compiler.source>17</maven.compiler.source> + <maven.compiler.target>17</maven.compiler.target> + </properties> + + <dependencies> + <!-- Jackson JSON Processor --> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <version>2.12.3</version> + </dependency> + + <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api --> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>2.0.16</version> + </dependency> + + + <!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic --> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-classic</artifactId> + <version>1.5.8</version> + </dependency> + + + <!-- Google Gson --> + <dependency> + <groupId>com.google.code.gson</groupId> + <artifactId>gson</artifactId> + <version>2.8.6</version> + </dependency> + + <!-- Your custom library dependency --> + <dependency> + <groupId>org.etsi.osl</groupId> + <artifactId>org.etsi.osl.model.tmf</artifactId> + <version>1.0.0</version> + </dependency> + </dependencies> + + <build> + <plugins> + <!-- Maven Compiler Plugin --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.8.1</version> + <configuration> + <source>17</source> + <target>17</target> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <version>3.2.0</version> + <configuration> + <archive> + <manifest> + <mainClass>org.osl.etsi.util.ServiceSpecificationFetcher</mainClass> + </manifest> + </archive> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-shade-plugin</artifactId> + <version>3.4.1</version> + <executions> + <execution> + <phase>package</phase> + <goals> + <goal>shade</goal> + </goals> + <configuration> + <createDependencyReducedPom>true</createDependencyReducedPom> + <transformers> + <!-- Ensures that the manifest specifies the main class --> + <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> + <mainClass>org.osl.etsi.util.ServiceSpecificationFetcher</mainClass> <!-- Fully Qualified Name --> + </transformer> + </transformers> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> + diff --git a/src/main/java/org/osl/etsi/util/KeycloakAuthenticator.java b/src/main/java/org/osl/etsi/util/KeycloakAuthenticator.java new file mode 100644 index 0000000..86ae9fd --- /dev/null +++ b/src/main/java/org/osl/etsi/util/KeycloakAuthenticator.java @@ -0,0 +1,175 @@ +package org.osl.etsi.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Properties; + +public class KeycloakAuthenticator { + + private static final Logger logger = LoggerFactory.getLogger(KeycloakAuthenticator.class.getName()); + + private final Properties config; + private final HttpClient client; + private final ObjectMapper objectMapper; + + private String currentToken; + private long tokenExpiryTime; + private long tokenRefreshBufferSeconds; + + /** + * Constructs a KeycloakAuthenticator with the specified configuration file path. + * + * @param configFilePath The file path to the configuration properties file. + * @throws IOException If there is an error loading the configuration file. + */ + public KeycloakAuthenticator(Properties config) throws IOException { + this.config = config; + this.client = HttpClient.newHttpClient(); + this.objectMapper = new ObjectMapper(); + setTokenRefreshBufferSeconds(); + } + + /** + * Loads the configuration properties from the specified file path. + * + * @param configFilePath The file path to the configuration properties file. + * @return The token refresh buffer time in seconds. + * @throws IOException If the configuration file cannot be read. + */ + private void setTokenRefreshBufferSeconds() throws IOException { + // Load the token refresh buffer time, defaulting to 60 seconds if not specified + String bufferSecondsStr = this.config.getProperty("token.refresh.buffer.seconds", "60"); + long bufferSeconds; + try { + bufferSeconds = Long.parseLong(bufferSecondsStr); + if (bufferSeconds < 0) { + throw new NumberFormatException("Buffer seconds cannot be negative."); + } + } catch (NumberFormatException ex) { + logger.warn("Invalid token.refresh.buffer.seconds value: " + bufferSecondsStr + ". Using default of 60 seconds."); + bufferSeconds = 60; + } + + logger.info("Token refresh buffer set to " + bufferSeconds + " seconds."); + this.tokenRefreshBufferSeconds=bufferSeconds; + } + + /** + * Retrieves a valid access token. If the current token is expired or not present, + * it authenticates with Keycloak to obtain a new one. + * + * @return A valid access token as a String. + * @throws IOException If an I/O error occurs during authentication. + * @throws InterruptedException If the HTTP request is interrupted. + */ + public synchronized String getToken() throws IOException, InterruptedException { + long currentEpochSeconds = Instant.now().getEpochSecond(); + + if (currentToken != null && currentEpochSeconds < (tokenExpiryTime - tokenRefreshBufferSeconds)) { + logger.info("Using cached token. Token expires at " + Instant.ofEpochSecond(tokenExpiryTime)); + return currentToken; + } else { + logger.info("Cached token is missing or nearing expiration. Authenticating to obtain a new token."); + return authenticateAndGetToken(); + } + } + + /** + * Authenticates with Keycloak and retrieves a new access token. + * + * @return The new access token as a String. + * @throws IOException If the authentication request fails. + * @throws InterruptedException If the HTTP request is interrupted. + */ + private String authenticateAndGetToken() throws IOException, InterruptedException { + String keycloakUrl = config.getProperty("keycloak.url"); + String clientId = config.getProperty("client.id"); + String clientSecret = config.getProperty("client.secret"); + String username = config.getProperty("username"); + String password = config.getProperty("password"); + + // Validate required properties + if (keycloakUrl == null || clientId == null || username == null || password == null) { + String errorMsg = "Missing required configuration properties."; + logger.error(errorMsg); + throw new IOException(errorMsg); + } + + // Build the form data with URL encoding + String form = buildFormData(clientId, clientSecret, username, password); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(keycloakUrl)) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString(form)) + .build(); + + logger.info("Sending authentication request to Keycloak."); + + HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200) { + JsonNode responseJson = objectMapper.readTree(response.body()); + currentToken = responseJson.get("access_token").asText(); + long expiresIn = responseJson.get("expires_in").asLong(); + + tokenExpiryTime = Instant.now().getEpochSecond() + expiresIn; + logger.info("Authentication successful. Token obtained. Token expires in " + expiresIn + " seconds."); + + return currentToken; + } else { + String errorMsg = "Authentication failed: HTTP status " + response.statusCode() + ": " + response.body(); + logger.error(errorMsg); + throw new IOException(errorMsg); + } + } + + /** + * Builds the URL-encoded form data for the authentication request. + * + * @param clientId The client ID. + * @param clientSecret The client secret (optional). + * @param username The username. + * @param password The password. + * @return The URL-encoded form data as a String. + * @throws IOException If URL encoding fails. + */ + private String buildFormData(String clientId, String clientSecret, String username, String password) throws IOException { + StringBuilder form = new StringBuilder(); + form.append("client_id=").append(urlEncode(clientId)); + if (clientSecret != null && !clientSecret.isEmpty()) { + form.append("&client_secret=").append(urlEncode(clientSecret)); + } + form.append("&username=").append(urlEncode(username)); + form.append("&password=").append(urlEncode(password)); + form.append("&grant_type=password"); + return form.toString(); + } + + /** + * URL-encodes a string using UTF-8 encoding. + * + * @param value The string to encode. + * @return The URL-encoded string. + * @throws IOException If UTF-8 encoding is not supported. + */ + private String urlEncode(String value) throws IOException { + try { + return URLEncoder.encode(value, StandardCharsets.UTF_8.toString()); + } catch (Exception ex) { + logger.error("URL encoding failed for value: " + value, ex); + throw new IOException("URL encoding failed.", ex); + } + } +} diff --git a/src/main/java/org/osl/etsi/util/ServiceSpecificationFetcher.java b/src/main/java/org/osl/etsi/util/ServiceSpecificationFetcher.java new file mode 100644 index 0000000..501497d --- /dev/null +++ b/src/main/java/org/osl/etsi/util/ServiceSpecificationFetcher.java @@ -0,0 +1,415 @@ +package org.osl.etsi.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.util.*; + +import org.etsi.osl.tmf.common.model.AttachmentRef; +import org.etsi.osl.tmf.common.model.AttachmentRefOrValue; +import org.etsi.osl.tmf.common.model.service.ServiceSpecificationRef; +import org.etsi.osl.tmf.lcm.model.LCMRuleSpecification; +import org.etsi.osl.tmf.rcm634.model.LogicalResourceSpecification; +import org.etsi.osl.tmf.rcm634.model.PhysicalResourceSpecification; +import org.etsi.osl.tmf.rcm634.model.ResourceSpecification; +import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationRef; +import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationRelationship; +import org.etsi.osl.tmf.scm633.model.ServiceSpecRelationship; +import org.etsi.osl.tmf.scm633.model.ServiceSpecification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ServiceSpecificationFetcher { + private static final Logger logger = LoggerFactory.getLogger(ServiceSpecificationFetcher.class); + private static final HttpClient client = HttpClient.newHttpClient(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static String apiEndpoint=null; + private static KeycloakAuthenticator keycloakAuthenticator = null; + private static final Properties config = new Properties(); + private static String configFilePath = null; + + public static void main(String[] args) throws IOException { + String serviceSpecUUid = null; + // Parse command-line arguments + for (int i = 0; i < args.length; i++) { + switch (args[i]) { + case "--configfile": + if (i + 1 < args.length) { + configFilePath = args[++i]; + logger.info("Config file path set to: " + configFilePath); + try (InputStream input = new FileInputStream(configFilePath)) { + config.load(input); + apiEndpoint = config.getProperty("sourceApiEndpoint.url"); + logger.info("Configuration loaded successfully from " + configFilePath); + } catch (IOException ex) { + logger.error("Failed to load configuration from " + configFilePath, ex); + throw ex; + } + keycloakAuthenticator= new KeycloakAuthenticator(config); + } else { + logger.error("Missing value for --configfile"); + printUsageAndExit(); + } + break; + case "--servicespecuuid": + if (i + 1 < args.length) { + serviceSpecUUid = args[++i]; + logger.info("Service spec uuid set to: " + serviceSpecUUid); + } else { + logger.error("Missing value for --servicespecuuid"); + printUsageAndExit(); + } + break; + default: + logger.error("Unknown argument: " + args[i]); + printUsageAndExit(); + } + } + + // Validate required arguments + if (configFilePath == null || serviceSpecUUid == null) { + logger.error("Missing required arguments."); + printUsageAndExit(); + } + + // Continue with the rest of your application logic + logger.info("Application started successfully with provided configurations."); + try { + fetchServiceSpecification(serviceSpecUUid, null); + logger.info("All data has been fetched and saved hierarchically."); + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } + + /** + * Prints usage instructions and exits the application. + */ + private static void printUsageAndExit() { + String usage = "Usage:\n" + + " java -jar servicespecificationfetcher.jar --configfile /path/to/config.yaml --servicespecuuid servicespecuuid\n\n" + + "Parameters:\n" + + " --configfile Path to the configuration file.\n" + + " --servicespecuuid Path to the service specification uuid."; + logger.info(usage); + System.exit(1); + } + + private static void loadConfig() { + try (InputStream input = ServiceSpecificationFetcher.class.getClassLoader().getResourceAsStream("config.properties")) { + if (input == null) { + throw new IOException("Unable to find config.properties"); + } + config.load(input); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + private static ServiceSpecification fetchServiceSpecification(String uuid, File parentDirectory) throws IOException, InterruptedException { + String url = apiEndpoint + "/serviceCatalogManagement/v4/serviceSpecification/" + uuid; + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); + HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new IOException("Failed to get a valid response for "+url+". HTTP status code: " + response.statusCode()); + } + + if (response.body() == null || response.body().isEmpty()) { + logger.error("No content to map for URL: " + url); + return null; + } + + ServiceSpecification serviceSpecification = objectMapper.readValue(response.body(), ServiceSpecification.class); + if (serviceSpecification != null && serviceSpecification.getId() != null) { + logger.info("ServiceSpecification found. Creating folder " + serviceSpecification.getId()); + File serviceSpecificationFolder = new File(parentDirectory, serviceSpecification.getId()); + if (!serviceSpecificationFolder.exists()) { + serviceSpecificationFolder.mkdirs(); + } + saveJsonToFile(serviceSpecification, serviceSpecificationFolder, serviceSpecification.getId() + ".json"); + fetchAndSaveAttachments(serviceSpecification, serviceSpecificationFolder); + fetchLCMRuleSpecification(uuid, serviceSpecificationFolder); + + File relationshipDir = new File(serviceSpecificationFolder, "serviceSpecificationServiceRelationships"); + if (!relationshipDir.exists()) { + relationshipDir.mkdirs(); + } + + if (serviceSpecification.getServiceSpecRelationship() != null) { + for (ServiceSpecRelationship serviceRelationship : serviceSpecification.getServiceSpecRelationship()) { + fetchServiceSpecification(serviceRelationship.getId(), relationshipDir); + } + } + + logger.info("Creating serviceSpecificationResourceRelationships folder for "+serviceSpecification.getId()); + + File resourceRelationshipDir = new File(serviceSpecificationFolder, "serviceSpecificationResourceRelationships"); + if (!resourceRelationshipDir.exists()) { + resourceRelationshipDir.mkdirs(); + } + + if (serviceSpecification.getResourceSpecification()!= null) { + logger.info("Resource Relationships for "+serviceSpecification.getId()+" found!"); + + for (ResourceSpecificationRef resourceRelationship : serviceSpecification.getResourceSpecification()) { + String relatedUrl = apiEndpoint + "/resourceCatalogManagement/v4/resourceSpecification/" + resourceRelationship.getId(); // Now using getId() + logger.info("Fetching from "+relatedUrl); + fetchResourceSpecification(relatedUrl, resourceRelationshipDir); + } + } + else + { + logger.info("Resource Relationships for "+serviceSpecification.getId()+" NOT found!"); + + } + } + return serviceSpecification; + } + + private static void fetchLCMRuleSpecification(String serviceSpecId, File parentDirectory) throws IOException, InterruptedException { + String url = apiEndpoint + "/lcmrulesmanagement/v1/lcmRuleSpecification/serviceSpec/" + serviceSpecId; + String token = keycloakAuthenticator.getToken(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Authorization", "Bearer " + token) + .GET() + .build(); + HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new IOException("Failed to get a valid response for "+url+". HTTP status code: " + response.statusCode()); + } + + File lcmRulesDirectory = new File(parentDirectory, "serviceSpecificationLcmRules"); + if (!lcmRulesDirectory.exists()) { + lcmRulesDirectory.mkdirs(); + } + + try { + List<String> ruleIds = parseJsonForIds(response.body()); + if (ruleIds != null && !ruleIds.isEmpty()) { + // Save a list of rule IDs to a file, assuming this is still required + //saveJsonToFile(ruleIds, new File(parentDirectory, serviceSpecId + "-lcmRuleIds.json")); + saveJsonToFile(response.body(), new File(parentDirectory, serviceSpecId + "-lcmRuleIds.json")); + + // Iterate over each ID to fetch and save detailed rule information + for (String ruleId : ruleIds) { + fetchAndSaveDetailedRule(ruleId, lcmRulesDirectory); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static void fetchAndSaveDetailedRule(String ruleId, File parentDirectory) throws IOException, InterruptedException { + String ruleUrl = apiEndpoint + "/lcmrulesmanagement/v1/lcmRuleSpecification/" + ruleId; + String token = keycloakAuthenticator.getToken(); + HttpRequest detailedRequest = HttpRequest.newBuilder() + .uri(URI.create(ruleUrl)) + .header("Authorization", "Bearer " + token) + .GET() + .build(); + HttpResponse<String> detailedResponse = client.send(detailedRequest, HttpResponse.BodyHandlers.ofString()); + + if (detailedResponse.statusCode() != 200) { + logger.error("Failed to get a valid response for rule ID: " + ruleId + " HTTP status code: " + detailedResponse.statusCode()); + return; + } + LCMRuleSpecification detailedRule = objectMapper.readValue(detailedResponse.body(), LCMRuleSpecification.class); + detailedRule.setServiceSpecs(new HashSet<ServiceSpecificationRef>()); + File ruleFile = new File(parentDirectory, ruleId + ".json"); + saveJsonToFile(detailedRule, ruleFile); + } + + private static ResourceSpecification fetchResourceSpecification(String url, File parentDirectory) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); + HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new IOException("Failed to get a valid response. HTTP status code: " + response.statusCode()); + } + + if (response.body() == null || response.body().isEmpty()) { + logger.error("No content to map for URL: " + url); + return null; + } + + ResourceSpecification resSpec = null; + try { + JsonNode rootNode = objectMapper.readTree(response.body()); + JsonNode typeNode = rootNode.get("@type"); + + if (typeNode != null) { + String type = typeNode.asText(); + ObjectMapper objectMapper = new ObjectMapper(); + if ("LogicalResourceSpecification".equals(type)) { + resSpec = objectMapper.readValue(response.body(), LogicalResourceSpecification.class); + logger.info("The JSON contains a LogicalResourceSpecification."); + } else if ("PhysicalResourceSpecification".equals(type)) { + resSpec = objectMapper.readValue(response.body(), PhysicalResourceSpecification.class); + logger.info("The JSON contains a PhysicalResourceSpecification."); + } else { + logger.info("The JSON contains an unknown type: " + type); + } + } else { + logger.info("The JSON does not contain a @type field."); + } + } catch (IOException e) { + e.printStackTrace(); + } + + if (resSpec != null && resSpec.getId() != null) { + logger.info("Creating "+resSpec.getId()+" folder!"); + //logger.info("Creating "+resSpec.getName()+" folder!"); + File resourceSpecificationFolder = new File(parentDirectory, resSpec.getId()); + //File resourceSpecificationFolder = new File(parentDirectory, resSpec.getName()); + if (!resourceSpecificationFolder.exists()) { + resourceSpecificationFolder.mkdirs(); + } + saveResourceSpecificationJsonToFile(resSpec, resourceSpecificationFolder, resSpec.getId() + ".json"); + //saveResourceSpecificationJsonToFile(resSpec, resourceSpecificationFolder, resSpec.getName() + ".json"); + fetchAndSaveAttachments(resSpec, resourceSpecificationFolder); + + File relationshipDir = new File(resourceSpecificationFolder, "resourceSpecificationResourceRelationships"); + if (!relationshipDir.exists()) { + relationshipDir.mkdirs(); + } + + if (resSpec.getResourceSpecRelationship()!= null) { + logger.info("Resource Relationships for "+resSpec.getId()+" found!"); + //logger.info("Resource Relationships for "+resSpec.getName()+" found!"); + for (ResourceSpecificationRelationship relationship : resSpec.getResourceSpecRelationship()) { + String relatedUrl = apiEndpoint + "/resourceCatalogManagement/v4/resourceSpecification/" + relationship.getId(); // Now using getId() + fetchResourceSpecification(relatedUrl, relationshipDir); + } + } + else + { + logger.info("Resource Relationships for "+resSpec.getId()+" NOT found!"); + + } + } + else + { + logger.info("resSpec != null && resSpec.getId() != null"); + } + return resSpec; + } + + // Method for ServiceSpecifications + private static void fetchAndSaveAttachments(ServiceSpecification spec, File parentFolder) throws IOException, InterruptedException { + if (spec.getAttachment() != null) { + for (AttachmentRef attachment : spec.getAttachment()) { + String attachmentUrl = attachment.getUrl(); + HttpRequest attachmentRequest = HttpRequest.newBuilder() + .uri(URI.create(apiEndpoint + attachmentUrl)) // Assuming the base URL needs to be prefixed + .GET() + .build(); + HttpResponse<byte[]> attachmentResponse = client.send(attachmentRequest, HttpResponse.BodyHandlers.ofByteArray()); + + if (attachmentResponse.statusCode() == 200) { + String relativePath = extractRelativePath(attachmentUrl); + File attachmentDir = new File(parentFolder, relativePath.substring(0, relativePath.lastIndexOf('/'))); + if (!attachmentDir.exists()) { + attachmentDir.mkdirs(); // Make sure the directory structure exists + } + File attachmentFile = new File(attachmentDir, relativePath.substring(relativePath.lastIndexOf('/') + 1)); + Files.write(attachmentFile.toPath(), attachmentResponse.body()); + logger.info("Attachment saved to " + attachmentFile.getPath()); + } else { + logger.info("Failed to fetch attachment at " + attachmentUrl); + } + } + } + } + + // Overloaded method for ResourceSpecifications + private static void fetchAndSaveAttachments(ResourceSpecification spec, File parentFolder) throws IOException, InterruptedException { + if (spec.getAttachment() != null) { + for (AttachmentRefOrValue attachment : spec.getAttachment()) { + String attachmentUrl = attachment.getUrl(); + HttpRequest attachmentRequest = HttpRequest.newBuilder() + .uri(URI.create(apiEndpoint + attachmentUrl)) // Assuming the base URL needs to be prefixed + .GET() + .build(); + HttpResponse<byte[]> attachmentResponse = client.send(attachmentRequest, HttpResponse.BodyHandlers.ofByteArray()); + + if (attachmentResponse.statusCode() == 200) { + String relativePath = extractRelativePath(attachmentUrl); + File attachmentDir = new File(parentFolder, relativePath.substring(0, relativePath.lastIndexOf('/'))); + if (!attachmentDir.exists()) { + attachmentDir.mkdirs(); // Make sure the directory structure exists + } + File attachmentFile = new File(attachmentDir, relativePath.substring(relativePath.lastIndexOf('/') + 1)); + Files.write(attachmentFile.toPath(), attachmentResponse.body()); + logger.info("Attachment saved to " + attachmentFile.getPath()); + } else { + logger.info("Failed to fetch attachment at " + attachmentUrl); + } + } + } + } + + public static List<String> parseJsonForIds(String json) throws IOException { + // Parse the JSON string into a JsonNode + JsonNode rootNode = objectMapper.readTree(json); + List<String> ids = new ArrayList<>(); + + // Check if the root node is an array and iterate over it + if (rootNode.isArray()) { + for (JsonNode node : rootNode) { + // Extract the id from each object in the array + String id = node.get("id").asText(); + ids.add(id); + } + } + + return ids; + } + + private static String extractRelativePath(String url) { + URI uri = URI.create(url); + String path = uri.getPath(); + return path.substring(path.indexOf("/attachment/") + 1); // Extract the path after "/serviceSpecification/" + } + + private static String extractFilename(String url) { + URI uri = URI.create(url); + String path = uri.getPath(); + return path.substring(path.lastIndexOf('/') + 1); + } + + private static void saveJsonToFile(ServiceSpecification spec, File parentFolder, String filename) throws IOException { + File file = new File(parentFolder, filename); + objectMapper.writerWithDefaultPrettyPrinter().writeValue(file, spec); + logger.info("Saved JSON to " + file.getPath()); + } + + private static void saveJsonToFile(Object data, File file) throws IOException { + objectMapper.writerWithDefaultPrettyPrinter().writeValue(file, data); + logger.info("Saved JSON to " + file.getPath()); + } + + private static void saveResourceSpecificationJsonToFile(ResourceSpecification spec, File parentFolder, String filename) throws IOException { + File file = new File(parentFolder, filename); + objectMapper.writerWithDefaultPrettyPrinter().writeValue(file, spec); + logger.info("Saved JSON to " + file.getPath()); + } +} diff --git a/src/main/resources/Dockerfile b/src/main/resources/Dockerfile new file mode 100644 index 0000000..594b20d --- /dev/null +++ b/src/main/resources/Dockerfile @@ -0,0 +1,17 @@ +# Select a base image with Java installed +FROM openjdk:17-slim + +# Set the working directory +WORKDIR /app + +# Copy the executable jar file of the application to the image +COPY servicespecificationfetcher-0.0.1-SNAPSHOT.jar /app/servicespecificationfetcher.jar + +# Set the command that will run when the container starts +# Note: Pass the data folder path as an environment variable or argument during runtime. + +# Example command to pass data folder as a runtime argument: +# docker run -v /local/path/to/config.properties:/app/config.properties -v /local/path/to/your-data-folder:/app/data-folder your-image uuid + +# Updated CMD to accept runtime arguments for data folder +ENTRYPOINT ["sh", "-c", "java -jar /app/servicespecificationfetcher.jar --configfile /app/config.properties --servicespecuuid $0"] \ No newline at end of file diff --git a/src/main/resources/config.properties b/src/main/resources/config.properties new file mode 100644 index 0000000..e10c7c4 --- /dev/null +++ b/src/main/resources/config.properties @@ -0,0 +1,7 @@ +keycloak.url=http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/token +client.id=osapiWebClientId +client.secret=admin +username=admin +password=admin +sourceApiEndpoint.url=http://localhost/tmf-api +serviceSpecification.uuid=487b9377-460d-4498-a8f3-a23cd7595b06 \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..e5a0c7c --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,20 @@ +<configuration> + <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern> + </encoder> + </appender> + + <appender name="FILE" class="ch.qos.logback.core.FileAppender"> + <file>app.log</file> + <append>true</append> + <encoder> + <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern> + </encoder> + </appender> + + <root level="debug"> + <appender-ref ref="CONSOLE" /> + <appender-ref ref="FILE" /> + </root> +</configuration> \ No newline at end of file -- GitLab