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