From 5cd86cf3f7cf7bffc675e24c06e49d7606c3bf98 Mon Sep 17 00:00:00 2001
From: Ioannis Chatzis <ioannis.chatzis@upatras.gr>
Date: Wed, 16 Oct 2024 14:54:05 +0300
Subject: [PATCH] Implementation Commit. Closes #1

---
 pom.xml                                       | 100 +++
 .../org/etsi/osl/util/JsonStringReplacer.java | 197 +++++
 .../etsi/osl/util/KeycloakAuthenticator.java  | 175 ++++
 .../ResourceSpecificationPayloadsMaker.java   |  60 ++
 .../ServiceSpecificationPayloadsMaker.java    | 188 +++++
 .../util/ServiceSpecificationUploader.java    | 753 ++++++++++++++++++
 src/main/java/org/etsi/osl/util/mappings.yaml |   7 +
 .../java/org/etsi/osl/util/mappings2.yaml     |   4 +
 src/main/resources/Dockerfile                 |  17 +
 src/main/resources/config.properties          |   6 +
 src/main/resources/logback.xml                |  20 +
 11 files changed, 1527 insertions(+)
 create mode 100644 pom.xml
 create mode 100644 src/main/java/org/etsi/osl/util/JsonStringReplacer.java
 create mode 100644 src/main/java/org/etsi/osl/util/KeycloakAuthenticator.java
 create mode 100644 src/main/java/org/etsi/osl/util/ResourceSpecificationPayloadsMaker.java
 create mode 100644 src/main/java/org/etsi/osl/util/ServiceSpecificationPayloadsMaker.java
 create mode 100644 src/main/java/org/etsi/osl/util/ServiceSpecificationUploader.java
 create mode 100644 src/main/java/org/etsi/osl/util/mappings.yaml
 create mode 100644 src/main/java/org/etsi/osl/util/mappings2.yaml
 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..90580b9
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,100 @@
+<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>servicespecificationuploader</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>
+
+        <!-- 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>
+
+        <!-- SnakeYAML -->
+        <dependency>
+            <groupId>org.yaml</groupId>
+            <artifactId>snakeyaml</artifactId>
+            <version>2.0</version>
+        </dependency>
+
+        <!-- Commons cli -->
+        <!-- https://mvnrepository.com/artifact/commons-cli/commons-cli -->
+        <dependency>
+            <groupId>commons-cli</groupId>
+            <artifactId>commons-cli</artifactId>
+            <version>1.9.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.etsi.osl.util.ServiceSpecificationUploader</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.etsi.osl.util.ServiceSpecificationUploader</mainClass> <!-- Fully Qualified Name -->
+                                </transformer>
+                            </transformers>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>  
+
diff --git a/src/main/java/org/etsi/osl/util/JsonStringReplacer.java b/src/main/java/org/etsi/osl/util/JsonStringReplacer.java
new file mode 100644
index 0000000..e671ab9
--- /dev/null
+++ b/src/main/java/org/etsi/osl/util/JsonStringReplacer.java
@@ -0,0 +1,197 @@
+package org.etsi.osl.util;
+
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.node.*;
+import org.yaml.snakeyaml.Yaml;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.util.*;
+
+public class JsonStringReplacer {
+
+    // Logger instance
+    private static final Logger logger = LoggerFactory.getLogger(JsonStringReplacer.class);
+
+    // Static variable to hold mappings
+    private static Map<String, String> mappings = new HashMap<>();
+
+    /**
+     * Sets the YAML file path and loads mappings.
+     * If the file is missing, it sets mappings to an empty map and continues.
+     *
+     * @param yamlFilePath The path to the YAML mappings file.
+     */
+    public static void setYamlFilePath(String yamlFilePath) {
+        try {
+            mappings = loadYamlMappings(yamlFilePath);
+        } catch (FileNotFoundException e) {
+            // Mappings file is missing; proceed with empty mappings
+            logger.warn("Mappings file not found: {}. Proceeding without replacements.", yamlFilePath);
+            mappings = new HashMap<>(); // Ensure mappings is empty
+        } catch (IOException e) {
+            // Other I/O errors
+            logger.error("Error reading mappings file: {}. Proceeding without replacements.", e.getMessage());
+            mappings = new HashMap<>(); // Ensure mappings is empty
+        } catch (Exception e) {
+            // Handle any other exceptions
+            logger.error("Unexpected error: {}. Proceeding without replacements.", e.getMessage(), e);
+            mappings = new HashMap<>(); // Ensure mappings is empty
+        }
+    }
+
+    /**
+     * Replaces strings in a JSON content based on the loaded mappings.
+     * If mappings are empty, it returns the original JSON content.
+     *
+     * @param jsonContent The input JSON content as a string.
+     * @return The modified JSON content as a string.
+     * @throws IOException If an I/O error occurs during processing.
+     */
+    public static String replaceJsonStrings(String jsonContent) throws IOException {
+        // If mappings are empty, return the original JSON content
+        if (mappings == null || mappings.isEmpty()) {
+            logger.info("Mappings are empty. Returning original JSON content.");
+            return jsonContent;
+        }
+
+        // Create an ObjectMapper for JSON parsing
+        ObjectMapper mapper = new ObjectMapper();
+
+        // Parse the JSON content into a JsonNode
+        JsonNode rootNode = mapper.readTree(jsonContent);
+
+        // Perform string replacements
+        replaceStrings(rootNode);
+
+        // Convert the modified JsonNode back into a JSON string
+        String result = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode);
+        logger.debug("Replacements completed successfully.");
+        return result;
+    }
+
+    /**
+     * Loads mappings from a YAML file.
+     *
+     * @param yamlFilePath The file path to the YAML mappings file.
+     * @return A Map containing the mappings.
+     * @throws IOException If an error occurs while reading the YAML file.
+     */
+    private static Map<String, String> loadYamlMappings(String yamlFilePath) throws IOException {
+        Yaml yaml = new Yaml();
+        Map<String, String> loadedMappings = new HashMap<>();
+
+        try (InputStream inputStream = new FileInputStream(yamlFilePath)) {
+            Map<String, Object> yamlMaps = yaml.load(inputStream);
+
+            if (yamlMaps == null || !yamlMaps.containsKey("mappings")) {
+                logger.warn("The YAML file does not contain a 'mappings' key. Proceeding without replacements.");
+                return loadedMappings; // Return empty mappings
+            }
+
+            Object mappingsObj = yamlMaps.get("mappings");
+
+            if (mappingsObj instanceof List) {
+                List<?> mappingsList = (List<?>) mappingsObj;
+                for (Object item : mappingsList) {
+                    if (item instanceof Map) {
+                        Map<?, ?> mapItem = (Map<?, ?>) item;
+                        Object fromObj = mapItem.get("from");
+                        Object toObj = mapItem.get("to");
+
+                        if (fromObj instanceof String && toObj instanceof String) {
+                            loadedMappings.put((String) fromObj, (String) toObj);
+                        } else {
+                            logger.warn("Invalid mapping entry: 'from' and 'to' must be strings. Skipping entry.");
+                        }
+                    } else {
+                        logger.warn("Invalid mapping format. Each mapping entry must be a map with 'from' and 'to' keys. Skipping entry.");
+                    }
+                }
+            } else if (mappingsObj instanceof Map) {
+                Map<?, ?> mappingsMap = (Map<?, ?>) mappingsObj;
+                for (Map.Entry<?, ?> entry : mappingsMap.entrySet()) {
+                    if (entry.getKey() instanceof String && entry.getValue() instanceof String) {
+                        loadedMappings.put((String) entry.getKey(), (String) entry.getValue());
+                    } else {
+                        logger.warn("Invalid mapping entry: keys and values must be strings. Skipping entry.");
+                    }
+                }
+            } else {
+                logger.warn("Unrecognized 'mappings' format in YAML file. Proceeding without replacements.");
+            }
+        }
+
+        logger.info("Loaded {} mappings from YAML file.", loadedMappings.size());
+        return loadedMappings;
+    }
+
+    /**
+     * Recursively replaces strings in a JsonNode based on the loaded mappings.
+     *
+     * @param node The JsonNode to process.
+     */
+    private static void replaceStrings(JsonNode node) {
+        if (node == null || mappings == null || mappings.isEmpty()) {
+            return;
+        }
+
+        if (node.isObject()) {
+            ObjectNode objNode = (ObjectNode) node;
+            Iterator<Map.Entry<String, JsonNode>> fields = objNode.fields();
+            while (fields.hasNext()) {
+                Map.Entry<String, JsonNode> entry = fields.next();
+                JsonNode childNode = entry.getValue();
+
+                if (childNode.isTextual()) {
+                    String textValue = childNode.asText();
+                    if (mappings.containsKey(textValue)) {
+                        String replacement = mappings.get(textValue);
+                        objNode.put(entry.getKey(), replacement);
+                        logger.debug("Replaced text '{}' with '{}' in object field '{}'.", textValue, replacement, entry.getKey());
+                    }
+                } else {
+                    replaceStrings(childNode);
+                }
+            }
+        } else if (node.isArray()) {
+            ArrayNode arrayNode = (ArrayNode) node;
+            for (int i = 0; i < arrayNode.size(); i++) {
+                JsonNode element = arrayNode.get(i);
+
+                if (element.isTextual()) {
+                    String textValue = element.asText();
+                    if (mappings.containsKey(textValue)) {
+                        String replacement = mappings.get(textValue);
+                        arrayNode.set(i, new TextNode(replacement));
+                        logger.debug("Replaced text '{}' with '{}' in array index {}.", textValue, replacement, i);
+                    }
+                } else {
+                    replaceStrings(element);
+                }
+            }
+        }
+        // Other node types are not modified
+    }
+
+    // Example usage in the main method
+    public static void main(String[] args) {
+        String jsonInput = "{ \"greeting\": \"string1\", \"farewell\": \"string2\", \"nested\": { \"message\": \"string3\" }, \"array\": [\"string1\", \"hello\", \"string2\"] }";
+        String yamlFilePath = "org/etsi/osl/util/mappings.yaml"; // Adjust the path as needed
+
+        // Set the YAML path from another class or context
+        JsonStringReplacer.setYamlFilePath(yamlFilePath);
+
+        try {
+            // Perform the replacement
+            String result = JsonStringReplacer.replaceJsonStrings(jsonInput);
+
+            // Output the modified JSON
+            System.out.println("Modified JSON:");
+            System.out.println(result);
+        } catch (IOException e) {
+            logger.error("An error occurred during JSON processing: {}", e.getMessage(), e);
+        }
+    }
+}
diff --git a/src/main/java/org/etsi/osl/util/KeycloakAuthenticator.java b/src/main/java/org/etsi/osl/util/KeycloakAuthenticator.java
new file mode 100644
index 0000000..85d41f7
--- /dev/null
+++ b/src/main/java/org/etsi/osl/util/KeycloakAuthenticator.java
@@ -0,0 +1,175 @@
+package org.etsi.osl.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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/etsi/osl/util/ResourceSpecificationPayloadsMaker.java b/src/main/java/org/etsi/osl/util/ResourceSpecificationPayloadsMaker.java
new file mode 100644
index 0000000..7f9778f
--- /dev/null
+++ b/src/main/java/org/etsi/osl/util/ResourceSpecificationPayloadsMaker.java
@@ -0,0 +1,60 @@
+package org.etsi.osl.util;
+
+import org.etsi.osl.tmf.common.model.TimePeriod;
+import org.etsi.osl.tmf.rcm634.model.ResourceSpecification;
+import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationCreate;
+import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationUpdate;
+
+import java.util.ArrayList;
+
+public class ResourceSpecificationPayloadsMaker {
+
+    // Change ServiceSpecificationCreate to ResourceSpecificationCreate
+    public static ResourceSpecificationCreate createResourceSpecDataFromFile(ResourceSpecification resourceSpecFromFile) {
+        // Parse the JSON file into a ServSpecCr object
+        ResourceSpecificationCreate newResourceSpec = new ResourceSpecificationCreate();
+        newResourceSpec.setName(resourceSpecFromFile.getName());
+        newResourceSpec.setDescription(resourceSpecFromFile.getDescription());
+        //newServSpec.setAttachment(new ArrayList<>(resourceSpecFromFile.getAttachment()));
+        newResourceSpec.setIsBundle(resourceSpecFromFile.isIsBundle());
+        //newServSpec.setRelatedParty(new ArrayList<>(resourceSpecFromFile.getRelatedParty()));
+        //newServSpec.setResourceSpecification(new ArrayList<>(resourceSpecFromFile.getResourceSpecification()));
+        //newServSpec.setServiceLevelSpecification(new ArrayList<>(resourceSpecFromFile.getServiceLevelSpecification()));
+        //newServSpec.setServiceSpecCharacteristic(new ArrayList<>(resourceSpecFromFile.getServiceSpecCharacteristic()));
+        //newServSpec.setServiceSpecRelationship(new ArrayList<>(resourceSpecFromFile.getServiceSpecRelationship()));
+        newResourceSpec.setVersion(resourceSpecFromFile.getVersion());
+        newResourceSpec.setValidFor(new TimePeriod());
+
+        if (resourceSpecFromFile.getType() != null) {
+            newResourceSpec.setType( resourceSpecFromFile.getType() );
+        }
+        return newResourceSpec;
+    }
+
+    public static ResourceSpecificationUpdate updateResourceSpecDataFromFile(ResourceSpecification resourceSpecFromFile) {
+        ResourceSpecificationUpdate newResourceSpecToUpdate = new ResourceSpecificationUpdate();
+
+        if (resourceSpecFromFile.getName() != null) {
+            newResourceSpecToUpdate.setName(resourceSpecFromFile.getName());
+        }
+
+        if (resourceSpecFromFile.getDescription() != null) {
+            newResourceSpecToUpdate.setDescription(resourceSpecFromFile.getDescription());
+        }
+
+        if (resourceSpecFromFile.isIsBundle() != null) {
+            newResourceSpecToUpdate.setIsBundle(resourceSpecFromFile.isIsBundle());
+        }
+
+        if (resourceSpecFromFile.getVersion() != null) {
+            newResourceSpecToUpdate.setVersion(resourceSpecFromFile.getVersion());
+        }
+
+        if (resourceSpecFromFile.getResourceSpecCharacteristic() != null) {
+            System.out.println("ServiceSpecCharacteristic: " + resourceSpecFromFile.getResourceSpecCharacteristic());
+            newResourceSpecToUpdate.setResourceSpecificationCharacteristic(new ArrayList<>(resourceSpecFromFile.getResourceSpecCharacteristic()));
+        }
+
+        return newResourceSpecToUpdate;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/etsi/osl/util/ServiceSpecificationPayloadsMaker.java b/src/main/java/org/etsi/osl/util/ServiceSpecificationPayloadsMaker.java
new file mode 100644
index 0000000..9576cdd
--- /dev/null
+++ b/src/main/java/org/etsi/osl/util/ServiceSpecificationPayloadsMaker.java
@@ -0,0 +1,188 @@
+package org.etsi.osl.util;
+
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.etsi.osl.tmf.common.model.Any;
+import org.etsi.osl.tmf.common.model.ELifecycle;
+import org.etsi.osl.tmf.common.model.EValueType;
+import org.etsi.osl.tmf.common.model.TimePeriod;
+import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationCharacteristic;
+import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationCharacteristicValue;
+import org.etsi.osl.tmf.scm633.model.*;
+
+public class ServiceSpecificationPayloadsMaker {
+
+	public static ServiceSpecificationCreate createServiceSpecDataFromFile(ServiceSpecification serviceSpecFromFile) {
+		// Parse the JSON file into a ServSpecCr object
+		ServiceSpecificationCreate newServSpec = new ServiceSpecificationCreate();
+		newServSpec.setName(serviceSpecFromFile.getName());
+		newServSpec.setDescription(serviceSpecFromFile.getDescription());
+		//newServSpec.setAttachment(new ArrayList<>(servSpec.getAttachment()));
+		newServSpec.setIsBundle(serviceSpecFromFile.isIsBundle());
+		//newServSpec.setRelatedParty(new ArrayList<>(servSpec.getRelatedParty()));
+		//newServSpec.setResourceSpecification(new ArrayList<>(servSpec.getResourceSpecification()));
+		//newServSpec.setServiceLevelSpecification(new ArrayList<>(servSpec.getServiceLevelSpecification()));
+		//newServSpec.setServiceSpecCharacteristic(new ArrayList<>(servSpec.getServiceSpecCharacteristic()));
+		//newServSpec.setServiceSpecRelationship(new ArrayList<>(servSpec.getServiceSpecRelationship()));
+		newServSpec.setVersion(serviceSpecFromFile.getVersion());
+		newServSpec.setValidFor(new TimePeriod());
+
+		if (serviceSpecFromFile.getType() != null) {
+			newServSpec.setType( serviceSpecFromFile.getType() );
+		}
+		return newServSpec;
+	}
+
+	public static ServiceSpecificationUpdate updateServiceSpecDataFromFile(ServiceSpecification serviceSpecFromFile) {
+		ServiceSpecificationUpdate newServiceSpecToUpdate = new ServiceSpecificationUpdate();
+
+		if (serviceSpecFromFile.getName() != null) {
+			newServiceSpecToUpdate.setName(serviceSpecFromFile.getName());
+		}
+
+		if (serviceSpecFromFile.getDescription() != null) {
+			newServiceSpecToUpdate.setDescription(serviceSpecFromFile.getDescription());
+		}
+
+		if (serviceSpecFromFile.isIsBundle() != null) {
+			newServiceSpecToUpdate.setIsBundle(serviceSpecFromFile.isIsBundle());
+		}
+
+		if (serviceSpecFromFile.getVersion() != null) {
+			newServiceSpecToUpdate.setVersion(serviceSpecFromFile.getVersion());
+		}
+
+		if (serviceSpecFromFile.getServiceSpecCharacteristic() != null) {
+			System.out.println("ServiceSpecCharacteristic: " + serviceSpecFromFile.getServiceSpecCharacteristic());
+			newServiceSpecToUpdate.setServiceSpecCharacteristic(new ArrayList<>(serviceSpecFromFile.getServiceSpecCharacteristic()));
+		}
+
+		return newServiceSpecToUpdate;
+	}
+
+	public static ServiceSpecificationUpdate getServiceSpecificationRelationshipsDataFromFile(ServiceSpecification serviceSpecFromFile, ServiceSpecification serviceSpecToPatch) {
+		ServiceSpecificationUpdate newServiceSpecToUpdate = new ServiceSpecificationUpdate();
+		if (newServiceSpecToUpdate.getServiceSpecRelationship() != null) {
+			newServiceSpecToUpdate.setServiceSpecRelationship((List<ServiceSpecRelationship>) serviceSpecFromFile.getServiceSpecRelationship());
+		}
+		return newServiceSpecToUpdate;
+	}
+
+	public static ServiceSpecification updateServiceSpecDataFromAPIcall(ServiceSpecification serviceSpecFromFile, ServiceSpecificationUpdate newServiceSpecToUpdate) {
+
+
+			if (newServiceSpecToUpdate.getName() != null) {
+				serviceSpecFromFile.setName(newServiceSpecToUpdate.getName());
+			}
+
+			if (newServiceSpecToUpdate.getDescription() != null) {
+				serviceSpecFromFile.setDescription(newServiceSpecToUpdate.getDescription());
+
+			}
+
+			if (newServiceSpecToUpdate.isIsBundle() != null) {
+				serviceSpecFromFile.isBundle(newServiceSpecToUpdate.isIsBundle());
+
+			}
+
+			serviceSpecFromFile.setLastUpdate(OffsetDateTime.now(ZoneOffset.UTC));
+
+
+			if (newServiceSpecToUpdate.getLifecycleStatus() != null) {
+				serviceSpecFromFile.setLifecycleStatusEnum(ELifecycle.getEnum(newServiceSpecToUpdate.getLifecycleStatus()));
+			}
+
+			if (newServiceSpecToUpdate.getVersion() != null) {
+				serviceSpecFromFile.setVersion(newServiceSpecToUpdate.getVersion());
+			}
+
+			/**
+			 * Update ServiceSpecCharacteristic list We need to compare by name, since IDs
+			 * will not exist
+			 */
+			if (newServiceSpecToUpdate.getServiceSpecCharacteristic() != null) {
+				// reattach attachments fromDB
+
+				Map<String, Boolean> idAddedUpdated = new HashMap<>();
+
+				for (ServiceSpecCharacteristic charUpd : newServiceSpecToUpdate.getServiceSpecCharacteristic()) {
+
+					boolean nameExists = false;
+					for (ServiceSpecCharacteristic originalSpecChar : serviceSpecFromFile.getServiceSpecCharacteristic()) {
+						if (originalSpecChar.getName()!=null && charUpd.getName()!=null && originalSpecChar.getName().equals(charUpd.getName())) {
+							nameExists = true;
+							idAddedUpdated.put(originalSpecChar.getName(), true);
+							originalSpecChar.updateWith(charUpd);
+							break;
+						}
+					}
+
+					if (!nameExists) {
+						serviceSpecFromFile.getServiceSpecCharacteristic().add(new ServiceSpecCharacteristic(charUpd));
+						if ( charUpd.getName() == null ) {
+							charUpd.setName( UUID.randomUUID().toString() );
+						}
+						idAddedUpdated.put(charUpd.getName(), true);
+					}
+
+				}
+
+				List<ServiceSpecCharacteristic> toRemove = new ArrayList<>();
+				for (ServiceSpecCharacteristic ss : serviceSpecFromFile.getServiceSpecCharacteristic()) {
+					if (idAddedUpdated.get(ss.getName()) == null) {
+						toRemove.add(ss);
+					}
+				}
+
+				for (ServiceSpecCharacteristic serviceSpecCharacteristic : toRemove) {
+					serviceSpecFromFile.getServiceSpecCharacteristic().remove(serviceSpecCharacteristic);
+				}
+
+			}
+
+			return serviceSpecFromFile;
+		}
+
+	private static ServiceSpecCharacteristic copyResourceCharacteristic(ResourceSpecificationCharacteristic sourceChar) {
+
+		ServiceSpecCharacteristic serviceSpecCharacteristicItem = new ServiceSpecCharacteristic();
+		serviceSpecCharacteristicItem.setDescription( sourceChar.getDescription());
+		serviceSpecCharacteristicItem.valueType( sourceChar.getValueType() );
+		serviceSpecCharacteristicItem.configurable(sourceChar.isConfigurable());
+		serviceSpecCharacteristicItem.setMinCardinality( sourceChar.getMinCardinality() );
+		serviceSpecCharacteristicItem.setMaxCardinality( sourceChar.getMaxCardinality()  );
+		serviceSpecCharacteristicItem.setValidFor( sourceChar.getValidFor() );
+		for (ResourceSpecificationCharacteristicValue cv : sourceChar.getResourceSpecCharacteristicValue()) {
+			ServiceSpecCharacteristicValue serviceSpecCharacteristicValueItem = new ServiceSpecCharacteristicValue();
+			serviceSpecCharacteristicValueItem.setValue( new Any( cv.getValue().getValue(), cv.getValue().getAlias()));
+			serviceSpecCharacteristicValueItem.isDefault( cv.isIsDefault() );
+			serviceSpecCharacteristicValueItem.setUnitOfMeasure( cv.getUnitOfMeasure() );
+			serviceSpecCharacteristicItem.addServiceSpecCharacteristicValueItem(serviceSpecCharacteristicValueItem );
+		}
+		return serviceSpecCharacteristicItem;
+	}
+
+
+	private static ServiceSpecCharacteristic addServiceSpecCharacteristic(ServiceSpecification serviceSpec, String aName, String description, Any any, EValueType eValueType) {
+		ServiceSpecCharacteristic serviceSpecCharacteristicItem = new ServiceSpecCharacteristic();
+		serviceSpecCharacteristicItem.setName( aName );
+		serviceSpecCharacteristicItem.setDescription(description);
+		serviceSpecCharacteristicItem.valueType( eValueType.getValue() );
+		serviceSpecCharacteristicItem.configurable(false);
+		serviceSpecCharacteristicItem.setMinCardinality(1);
+		serviceSpecCharacteristicItem.setMaxCardinality(1);
+		ServiceSpecCharacteristicValue serviceSpecCharacteristicValueItem = new ServiceSpecCharacteristicValue();
+		serviceSpecCharacteristicValueItem.setValue( new Any( any.getValue() , any.getAlias()));
+		serviceSpecCharacteristicValueItem.isDefault( true );
+		serviceSpecCharacteristicValueItem.setUnitOfMeasure("N/A");
+		serviceSpecCharacteristicItem.addServiceSpecCharacteristicValueItem(serviceSpecCharacteristicValueItem );
+		serviceSpec.addServiceSpecCharacteristicItem(serviceSpecCharacteristicItem );
+		return serviceSpecCharacteristicItem;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/etsi/osl/util/ServiceSpecificationUploader.java b/src/main/java/org/etsi/osl/util/ServiceSpecificationUploader.java
new file mode 100644
index 0000000..6c8feb5
--- /dev/null
+++ b/src/main/java/org/etsi/osl/util/ServiceSpecificationUploader.java
@@ -0,0 +1,753 @@
+package org.etsi.osl.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+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.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.stream.Stream;
+
+import org.etsi.osl.tmf.common.model.service.ServiceSpecificationRef;
+import org.etsi.osl.tmf.lcm.model.LCMRuleSpecification;
+import org.etsi.osl.tmf.rcm634.model.*;
+import org.etsi.osl.tmf.scm633.model.ServiceSpecRelationship;
+import org.etsi.osl.tmf.scm633.model.ServiceSpecification;
+import org.etsi.osl.tmf.scm633.model.ServiceSpecificationCreate;
+import org.etsi.osl.tmf.scm633.model.ServiceSpecificationUpdate;
+import org.etsi.osl.tmf.lcm.model.LCMRuleSpecificationCreate;
+
+import java.io.File;
+
+public class ServiceSpecificationUploader {
+    private static final Logger logger = LoggerFactory.getLogger(ServiceSpecificationUploader.class);
+    private static final HttpClient client = HttpClient.newHttpClient();
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+    private static String apiEndpoint = null;
+    private static final Map<String, String> uuidmap = new HashMap<>();
+    private static final Map<String, String> idmap = new HashMap<>();
+    private static final Map<String, String> uuid2nameversion = new HashMap<>();
+    private static String configFilePath = null;
+    private static KeycloakAuthenticator keycloakAuthenticator = null;
+    private static final Properties config = new Properties();
+
+    public static void main(String[] args) throws IOException {
+        String serviceSpecFolderPath = 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("targetApiEndpoint.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 "--servicespecfolderpath":
+                    if (i + 1 < args.length) {
+                        serviceSpecFolderPath = args[++i];
+                        logger.info("Service spec folder path set to: " + serviceSpecFolderPath);
+                    } else {
+                        logger.error("Missing value for --servicespecfolderpath");
+                        printUsageAndExit();
+                    }
+                    break;
+                default:
+                    logger.error("Unknown argument: " + args[i]);
+                    printUsageAndExit();
+            }
+        }
+
+        // Validate required arguments
+        if (configFilePath == null || serviceSpecFolderPath == null) {
+            logger.error("Missing required arguments.");
+            printUsageAndExit();
+        }
+
+        // Load and process the service specifications
+        File serviceSpecDir = new File(serviceSpecFolderPath);
+        if (!serviceSpecDir.exists() || !serviceSpecDir.isDirectory()) {
+            logger.error("Service specification folder does not exist or is not a directory.");
+            System.exit(1);
+        }
+
+        File[] serviceSpecFiles = serviceSpecDir.listFiles();
+        if (serviceSpecFiles != null) {
+            logger.info("Service Specification Files:");
+            for (File file : serviceSpecFiles) {
+                if (file.isFile()) {
+                    logger.info("- " + file.getName());
+                }
+            }
+        } else {
+            logger.error("Unable to list files in the service specification folder.");
+            System.exit(1);
+        }
+
+        // Continue with the rest of your application logic
+        logger.info("Application started successfully with provided configurations.");
+
+    	//String jsonFilesPath = "C:/openslice/servicespecificationfetcher/e83a4e2e-7036-44fa-8bd2-e0ddc954c63d"; //env.getProperty("json.files.path");
+        logger.info("JSON Files Path: {}", serviceSpecFolderPath);
+		Path initialPath = Paths.get(serviceSpecFolderPath);
+        // Set the YAML path from another class or context
+        logger.info("mappings YAML Files Path: {}", initialPath.resolve("mappings.yaml"));
+        JsonStringReplacer.setYamlFilePath(initialPath.resolve("mappings.yaml").toString());
+        // Example of processing the JSON files
+        processServiceSpecificationRecursively(initialPath);
+    }
+
+    /**
+     * Prints usage instructions and exits the application.
+     */
+    private static void printUsageAndExit() {
+        String usage = "Usage:\n" +
+                "  java -jar myapplication.jar --configfile /path/to/config.yaml --servicespecfolderpath /path/to/servicespec/\n\n" +
+                "Parameters:\n" +
+                "  --configfile               Path to the configuration file.\n" +
+                "  --servicespecfolderpath    Path to the service specification folder.";
+        logger.info(usage);
+        System.exit(1);
+    }
+
+    private static void processServiceSpecificationRecursively(Path directoryPath) {
+
+        logger.info("Going for ServiceRelationships Recursively at {}", directoryPath);
+    	// Iterate for all subfolders in serviceSpecificationServiceRelationships
+        try (Stream<Path> serviceSpecificationServiceRelationshipsPaths = Files.list(directoryPath.resolve("serviceSpecificationServiceRelationships"))) {
+        	// As long as we find serviceSpecificationServiceRelationships we continue in depth
+        	serviceSpecificationServiceRelationshipsPaths
+                .filter(Files::isDirectory)
+                .forEach(serviceSpecificationServiceRelationshipsPathSubDir -> processServiceSpecificationRecursively(serviceSpecificationServiceRelationshipsPathSubDir));
+            
+            // As long as we don't find serviceSpecificationServiceRelationships we continue with ResourceRelationships
+            logger.info("Going for ResourceRelationships Recursively at {}", directoryPath);
+            try (Stream<Path> serviceSpecificationResourceRelationshipsPaths = Files.list(directoryPath.resolve("serviceSpecificationResourceRelationships"))) {
+            	serviceSpecificationResourceRelationshipsPaths
+	            .filter(Files::isDirectory)
+	            .forEach(serviceSpecificationResourceRelationshipsSubDir -> processResourceRelationshipsRecursively(serviceSpecificationResourceRelationshipsSubDir));            
+            } catch (IOException e) {
+                logger.error("Failed to read directory: {} - {}", directoryPath, e.getMessage());
+            }
+
+            // Process JSON files in the current directory after processing subdirectories
+            logger.info("Going for JSON files at {}", directoryPath);
+            try (Stream<Path> files = Files.list(directoryPath)) {
+                files
+                    .filter(Files::isRegularFile)
+                    .filter(path -> path.toString().endsWith(".json") && !path.toString().endsWith("-lcmRuleIds.json"))
+                    .forEach(path -> {
+                        logger.info("Posting serviceSpecificationServiceRelationships json info at path {}", path);
+                        postServiceSpec(path);
+                    });
+            }
+        } catch (IOException e) {
+            logger.error("Failed to read directory: {} - {}", directoryPath, e.getMessage());
+        }
+    }
+    private static void processResourceRelationshipsRecursively(Path directoryPath) {
+        logger.info("Going for ResourceRelationships Recursively at {}", directoryPath);
+        // As long as we find resourceSpecificationResourceRelationships we continue in depth
+        String appendSubpath = "serviceSpecificationResourceRelationships";
+        if(directoryPath.toString().contains("serviceSpecificationResourceRelationships"))
+        {
+            appendSubpath="resourceSpecificationResourceRelationships";
+        }
+
+        try (Stream<Path> paths = Files.list(directoryPath.resolve(appendSubpath))) {
+            paths
+                    .filter(Files::isDirectory)
+                    .forEach(subDir -> processResourceRelationshipsRecursively(subDir));
+
+            // As long as we don't find resourceSpecificationResourceRelationships we continue with ResourceRelationship installation
+            try (Stream<Path> files = Files.list(directoryPath)) {
+                files
+                        .filter(Files::isRegularFile)
+                        .filter(path -> path.toString().endsWith(".json"))
+                        .forEach(path -> {
+                            logger.info("Working for ResourceSpecification on {}", path);
+                            ObjectMapper objectMapper = new ObjectMapper();
+                            ResourceSpecification resSpec = null;
+                            JsonNode rootNode = null;
+                            try {
+                                rootNode = objectMapper.readTree(path.toFile());
+                            } catch (IOException e) {
+                                e.printStackTrace();
+                            }
+                            JsonNode typeNode = rootNode.get("@type");
+                            String type = typeNode.asText();
+                            if ("LogicalResourceSpecification".equals(type)) {
+                                logger.info("The JSON contains a LogicalResourceSpecification.");
+                                postLogicalResourceSpec(path);
+                            } else if ("PhysicalResourceSpecification".equals(type)) {
+                                logger.info("The JSON contains a PhysicalResourceSpecification.");
+                                postPhysicalResourceSpec(path);
+                            }
+                            else {
+                                logger.error("The JSON contains an unknown type: {}", type);
+                            }
+                        });
+            }
+
+        } catch (IOException e) {
+            logger.error("Failed to read directory: {} - {}", directoryPath, e.getMessage());
+        }
+    }
+
+    private static HttpResponse<String> sendHttpRequestWithRetry(String url, String token, String jsonContent, String method, String methodName, int maxRetries) throws IOException, InterruptedException {
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(url))
+                .header("Content-Type", "application/json")
+                .header("Authorization", "Bearer " + token)
+                .method(method, HttpRequest.BodyPublishers.ofString(jsonContent))
+                .build();
+
+        int attempt = 0;
+        HttpResponse<String> response;
+        do {
+            response = client.send(request, HttpResponse.BodyHandlers.ofString());
+            if (response.statusCode() == 200) {
+                return response;
+            }
+            attempt++;
+        } while (attempt < maxRetries);
+
+        throw new IOException("Failed request with status code: " + response.statusCode());
+    }
+
+    public static List<ResourceSpecification> getResourceSpecificationsInOpenslice() throws IOException, InterruptedException {
+        String url = apiEndpoint + "/resourceCatalogManagement/v4/resourceSpecification";
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(url))
+                .GET()
+                .build();
+
+        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+        if (response.statusCode() == 200) {
+            List<ResourceSpecification> resourceSpecifications = new ArrayList<>();
+            JsonNode rootNode = objectMapper.readTree(response.body());
+
+            for (JsonNode node : rootNode) {
+                String type = node.get("@type").asText();
+                ResourceSpecification resourceSpecification;
+
+                if ("LogicalResourceSpecification".equals(type)) {
+                    resourceSpecification = objectMapper.treeToValue(node, LogicalResourceSpecification.class);
+                } else if ("PhysicalResourceSpecification".equals(type)) {
+                    resourceSpecification = objectMapper.treeToValue(node, PhysicalResourceSpecification.class);
+                } else {
+                    throw new IllegalArgumentException("Unsupported ResourceSpecification type: " + type);
+                }
+                resourceSpecifications.add(resourceSpecification);
+            }
+            return resourceSpecifications;
+        } else {
+            throw new IOException("Failed to get service specifications. HTTP status code: " + response.statusCode());
+        }
+    }
+
+    public static List<ServiceSpecification> getServiceSpecificationsInOpenslice() throws IOException, InterruptedException {
+        String url = apiEndpoint + "/serviceCatalogManagement/v4/serviceSpecification";
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(url))
+                .GET()
+                .build();
+
+        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+        if (response.statusCode() == 200) {
+            return objectMapper.readValue(response.body(), new TypeReference<List<ServiceSpecification>>() {});
+        } else {
+            throw new IOException("Failed to get service specifications. HTTP status code: " + response.statusCode());
+        }
+    }
+
+    private static void postServiceSpec(Path path) {
+    	try {
+            ServiceSpecification servSpecFromFile = objectMapper.readValue(path.toFile(), ServiceSpecification.class);
+            // If the ServiceSpecification name and version already exists in the database, skip it to avoid duplicates
+            if (uuid2nameversion.containsValue(servSpecFromFile.getName() + " " + servSpecFromFile.getVersion())) {
+                logger.info("ServiceSpecification with name {} and version {} already exists. Skipping.", servSpecFromFile.getName(), servSpecFromFile.getVersion());
+                return;
+            }
+            // Use getServiceSpecificationsInOpenslice to get the available ServiceSpecifications in OpenSlice and chech if the servSpecFromFile exist in OPenslice. Check by comparing name and version.
+            List<ServiceSpecification> serviceSpecificationsInOpenslice = getServiceSpecificationsInOpenslice();
+            for (ServiceSpecification serviceSpecificationInOpenSlice : serviceSpecificationsInOpenslice) {
+                if (serviceSpecificationInOpenSlice.getName().equals(servSpecFromFile.getName()) && serviceSpecificationInOpenSlice.getVersion().equals(servSpecFromFile.getVersion())) {
+                    uuidmap.put(servSpecFromFile.getUuid(), serviceSpecificationInOpenSlice.getUuid());
+                    idmap.put(servSpecFromFile.getId(), serviceSpecificationInOpenSlice.getId());
+                    uuid2nameversion.put(serviceSpecificationInOpenSlice.getUuid(), serviceSpecificationInOpenSlice.getName() + " " + serviceSpecificationInOpenSlice.getVersion());
+                    logger.info("ServiceSpecification with name {} and version {} already exists. Skipping.", servSpecFromFile.getName(), servSpecFromFile.getVersion());
+                    return;
+                }
+            }
+
+            logger.info("ServiceSpecification posting main properties.");
+            ServiceSpecification newServSpec = postServiceSpecMainProperties(servSpecFromFile);
+            uuidmap.put(servSpecFromFile.getUuid(), newServSpec.getUuid());
+            idmap.put(servSpecFromFile.getId(), newServSpec.getId());
+            uuid2nameversion.put(newServSpec.getUuid(), newServSpec.getName() + " " + newServSpec.getVersion());
+            newServSpec = patchServiceSpecificationCharacteristics(servSpecFromFile, newServSpec);
+            postLCMRules(path, newServSpec);
+            postAttachment(path, newServSpec);
+            newServSpec = patchServiceSpecificationServiceRelationships(servSpecFromFile, newServSpec);
+            newServSpec = patchServiceSpecificationResourceRelationships(servSpecFromFile, newServSpec);
+        } catch (IOException | InterruptedException e) {
+            logger.info("Process halted due to failure: {}", e.getMessage());
+        }
+	}
+
+    private static ResourceSpecification patchResourceSpecificationResourceRelationships(ResourceSpecification resourceSpecFromFile, ResourceSpecification resourceSpecToPatch) throws IOException, InterruptedException {
+        // Check if servSpecUpdate has serviceSpecRelationship. If there is no serviceSpecRelationship, skip the patch
+        if (resourceSpecFromFile.getResourceSpecRelationship() == null || resourceSpecFromFile.getResourceSpecRelationship().isEmpty()) {
+            return resourceSpecToPatch;
+        }
+        ResourceSpecificationUpdate newResourceSpecUpdate = new ResourceSpecificationUpdate();
+        List<ResourceSpecificationRelationship> newResourceSpecRelationship = new ArrayList<>();
+        //Iterate over servSpecUpdate and replace uuids with the new ones
+        for (ResourceSpecificationRelationship rsr : resourceSpecFromFile.getResourceSpecRelationship()) {
+            if (idmap.containsKey(rsr.getId())) {
+                rsr.setId(idmap.get(rsr.getId()));
+                rsr.setUuid(uuidmap.get(rsr.getId()));
+            }
+            else {
+                logger.error("Failed to find id: {}", rsr.getId());
+            }
+
+            newResourceSpecRelationship.add(rsr);
+        }
+        newResourceSpecUpdate.setResourceSpecificationRelationship(newResourceSpecRelationship);
+        String jsonContent = JsonStringReplacer.replaceJsonStrings(objectMapper.writeValueAsString(newResourceSpecUpdate));
+        String ruleUrl = apiEndpoint + "/resourceCatalogManagement/v4/resourceSpecification/" + resourceSpecToPatch.getUuid();
+        String token = keycloakAuthenticator.getToken();
+
+        HttpResponse<String> response = sendHttpRequestWithRetry(ruleUrl, token, jsonContent, "PATCH", "patch Resource Specification", 3);
+
+        if (response.statusCode() != 200) {
+            throw new IOException("Failed request with status code: " + response.statusCode());
+        } else {
+            JsonNode rootNode = objectMapper.readTree(response.body());
+            String baseType = rootNode.get("@type").asText();
+            if ("LogicalResourceSpecification".equals(baseType)) {
+                return objectMapper.treeToValue(rootNode, LogicalResourceSpecification.class);
+            } else if ("PhysicalResourceSpecification".equals(baseType)) {
+                return objectMapper.treeToValue(rootNode, PhysicalResourceSpecification.class);
+            } else {
+                throw new IllegalArgumentException("Unsupported ResourceSpecification baseType: " + baseType);
+            }
+        }
+    }
+
+    private static ServiceSpecification patchServiceSpecificationServiceRelationships(ServiceSpecification servSpecFromFile, ServiceSpecification servSpecToPatch) throws IOException, InterruptedException {
+        // Check if servSpecUpdate has serviceSpecRelationship. If there is no serviceSpecRelationship, skip the patch
+        if (servSpecFromFile.getServiceSpecRelationship() == null || servSpecFromFile.getServiceSpecRelationship().isEmpty()) {
+            return servSpecToPatch;
+        }
+        ServiceSpecificationUpdate newServiceSpecUpdate = new ServiceSpecificationUpdate();
+        List<ServiceSpecRelationship> newServiceSpecRelationship = new ArrayList<>();
+        //Iterate over servSpecUpdate and replace uuids with the new ones
+        for (ServiceSpecRelationship ssr : servSpecFromFile.getServiceSpecRelationship()) {
+            if (idmap.containsKey(ssr.getId())) {
+                ssr.setId(idmap.get(ssr.getId()));
+                ssr.setUuid(uuidmap.get(ssr.getId()));
+            }
+            else {
+                logger.error("Failed to find id: {}", ssr.getId());
+            }
+
+            newServiceSpecRelationship.add(ssr);
+        }
+        newServiceSpecUpdate.setServiceSpecRelationship(newServiceSpecRelationship);
+        String jsonContent = JsonStringReplacer.replaceJsonStrings(objectMapper.writeValueAsString(newServiceSpecUpdate));
+        String ruleUrl = apiEndpoint + "/serviceCatalogManagement/v4/serviceSpecification/" + servSpecToPatch.getUuid();
+        String token = keycloakAuthenticator.getToken();
+
+        HttpResponse<String> response = sendHttpRequestWithRetry(ruleUrl, token, jsonContent, "PATCH", "patch Service Specification", 3);
+
+        if (response.statusCode() != 200) {
+            throw new IOException("Failed request with status code: " + response.statusCode());
+        } else {
+            return objectMapper.readValue(response.body(), ServiceSpecification.class);
+        }
+    }
+
+    private static ServiceSpecification patchServiceSpecificationResourceRelationships(ServiceSpecification servSpecFromFile, ServiceSpecification servSpecToPatch) throws IOException, InterruptedException {
+        // Check if servSpecUpdate has resourceSpecRelationship. If there is no resourceSpecRelationship, skip the patch
+        if (servSpecFromFile.getResourceSpecification() == null || servSpecFromFile.getResourceSpecification().isEmpty()) {
+            return servSpecToPatch;
+        }
+        ServiceSpecificationUpdate newServiceSpecUpdate = new ServiceSpecificationUpdate();
+        List<ResourceSpecificationRef> newServiceSpecResourceRelationship = new ArrayList<>();
+        //Iterate over servSpecUpdate and replace uuids with the new ones
+        for (ResourceSpecificationRef rsr : servSpecFromFile.getResourceSpecification()) {
+            if (idmap.containsKey(rsr.getId())) {
+                rsr.setId(idmap.get(rsr.getId()));
+                rsr.setUuid(uuidmap.get(rsr.getId()));
+            }
+            else {
+                logger.error("Failed to find id: {}", rsr.getId());
+            }
+
+            newServiceSpecResourceRelationship.add(rsr);
+        }
+        newServiceSpecUpdate.setResourceSpecification(newServiceSpecResourceRelationship);
+        String jsonContent = JsonStringReplacer.replaceJsonStrings(objectMapper.writeValueAsString(newServiceSpecUpdate));
+        String ruleUrl = apiEndpoint + "/serviceCatalogManagement/v4/serviceSpecification/" + servSpecToPatch.getUuid();
+        String token = keycloakAuthenticator.getToken();
+
+        HttpResponse<String> response = sendHttpRequestWithRetry(ruleUrl, token, jsonContent, "PATCH", "patch Service Specification", 3);
+
+        if (response.statusCode() != 200) {
+            throw new IOException("Failed request with status code: " + response.statusCode());
+        } else {
+            return objectMapper.readValue(response.body(), ServiceSpecification.class);
+        }
+    }
+
+    private static void postAttachment(Path path, ServiceSpecification newServSpec) {
+        Path basePath = Files.isDirectory(path) ? path : path.getParent();
+        logger.info("BasePath for attachments: {}", basePath);
+        Path attachmentsPath = basePath.resolve("attachment");
+        if (!Files.exists(attachmentsPath) || !Files.isDirectory(attachmentsPath)) {
+            logger.info("Directory {} does not exist. Skipping Attachments posting.", attachmentsPath);
+            return;
+        }
+
+        try (Stream<Path> subDirs = Files.list(attachmentsPath)) {
+            subDirs
+                    .filter(Files::isDirectory)
+                    .forEach(subDir -> {
+                        try (Stream<Path> files = Files.list(subDir)) {
+                            files
+                                    .filter(Files::isRegularFile)
+                                    .forEach(file -> {
+                                        try {
+                                            byte[] fileContent = Files.readAllBytes(file);
+                                            String attachmentUrl = apiEndpoint + "/serviceCatalogManagement/v4/serviceSpecification/" + newServSpec.getUuid() + "/attachment";
+                                            String token = keycloakAuthenticator.getToken();
+
+                                            String boundary = "----WebKitFormBoundaryqGHQuzNBYYgFlLIr";
+                                            String formData = "--" + boundary + "\r\n" +
+                                                    "Content-Disposition: form-data; name=\"afile\"; filename=\"" + file.getFileName() + "\"\r\n" +
+                                                    "Content-Type: application/octet-stream\r\n\r\n" +
+                                                    new String(fileContent) + "\r\n" +
+                                                    "--" + boundary + "--";
+
+                                            HttpRequest request = HttpRequest.newBuilder()
+                                                    .uri(URI.create(attachmentUrl))
+                                                    .header("Authorization", "Bearer " + token)
+                                                    .header("Content-Type", "multipart/form-data; boundary=" + boundary)
+                                                    .header("accept", "application/json, text/plain, */*")
+                                                    .header("accept-encoding", "gzip, deflate, br, zstd")
+                                                    .POST(HttpRequest.BodyPublishers.ofString(formData))
+                                                    .build();
+
+                                            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+                                            if (response.statusCode() != 200) {
+                                                logger.error("Failed to post attachment. HTTP status code: {}", response.statusCode());
+                                                logger.error("Response body: {}", response.body());
+                                            } else {
+                                                logger.info("Successfully posted attachment. Response: {}", response.body());
+                                            }
+                                        } catch (IOException | InterruptedException e) {
+                                            logger.error("Failed to process file: {} - {}", file, e.getMessage());
+                                        }
+                                    });
+                        } catch (IOException e) {
+                            logger.error("Failed to read directory: {} - {}", subDir, e.getMessage());
+                        }
+                    });
+        } catch (IOException e) {
+            logger.error("Failed to read directory: {} - {}", attachmentsPath, e.getMessage());
+        }
+    }
+
+    private static void postResourceSpecificationAttachment(Path path, ResourceSpecification newResourceSpec) {
+        Path basePath = Files.isDirectory(path) ? path : path.getParent();
+        logger.info("BasePath for attachments: {}", basePath);
+        Path attachmentsPath = basePath.resolve("attachment");
+        if (!Files.exists(attachmentsPath) || !Files.isDirectory(attachmentsPath)) {
+            logger.info("Directory {} does not exist. Skipping Attachments posting.", attachmentsPath);
+            return;
+        }
+
+        try (Stream<Path> subDirs = Files.list(attachmentsPath)) {
+            subDirs
+                    .filter(Files::isDirectory)
+                    .forEach(subDir -> {
+                        try (Stream<Path> files = Files.list(subDir)) {
+                            files
+                                    .filter(Files::isRegularFile)
+                                    .forEach(file -> {
+                                        try {
+                                            byte[] fileContent = Files.readAllBytes(file);
+                                            String attachmentUrl = apiEndpoint + "/resourceCatalogManagement/v4/resourceSpecification/" + newResourceSpec.getUuid() + "/attachment";
+                                            String token = keycloakAuthenticator.getToken();
+
+                                            String boundary = "----WebKitFormBoundaryqGHQuzNBYYgFlLIr";
+                                            String formData = "--" + boundary + "\r\n" +
+                                                    "Content-Disposition: form-data; name=\"afile\"; filename=\"" + file.getFileName() + "\"\r\n" +
+                                                    "Content-Type: application/octet-stream\r\n\r\n" +
+                                                    new String(fileContent) + "\r\n" +
+                                                    "--" + boundary + "--";
+
+                                            HttpRequest request = HttpRequest.newBuilder()
+                                                    .uri(URI.create(attachmentUrl))
+                                                    .header("Authorization", "Bearer " + token)
+                                                    .header("Content-Type", "multipart/form-data; boundary=" + boundary)
+                                                    .header("accept", "application/json, text/plain, */*")
+                                                    .header("accept-encoding", "gzip, deflate, br, zstd")
+                                                    .POST(HttpRequest.BodyPublishers.ofString(formData))
+                                                    .build();
+
+                                            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+                                            if (response.statusCode() != 200) {
+                                                logger.error("Failed to post attachment. HTTP status code: {}", response.statusCode());
+                                                logger.error("Response body: {}", response.body());
+                                            } else {
+                                                logger.info("Successfully posted attachment. Response: {}", response.body());
+                                            }
+                                        } catch (IOException | InterruptedException e) {
+                                            logger.error("Failed to process file: {} - {}", file, e.getMessage());
+                                        }
+                                    });
+                        } catch (IOException e) {
+                            logger.error("Failed to read directory: {} - {}", subDir, e.getMessage());
+                        }
+                    });
+        } catch (IOException e) {
+            logger.error("Failed to read directory: {} - {}", attachmentsPath, e.getMessage());
+        }
+    }
+
+    private static void postLCMRules(Path path, ServiceSpecification newServSpec) {
+        Path basePath = Files.isDirectory(path) ? path : path.getParent();
+        Path lcmRulesPath = basePath.resolve("serviceSpecificationLcmRules");
+
+        if (!Files.exists(lcmRulesPath) || !Files.isDirectory(lcmRulesPath)) {
+            logger.info("Directory {} does not exist. Skipping LCM Rule posting.", lcmRulesPath);
+            return;
+        }
+
+        try (Stream<Path> files = Files.list(lcmRulesPath)) {
+            files
+                    .filter(Files::isRegularFile)
+                    .filter(file -> file.toString().endsWith(".json"))
+                    .forEach(file -> {
+                        try {
+                            LCMRuleSpecification lcmRule = objectMapper.readValue(file.toFile(), LCMRuleSpecification.class);
+                            LCMRuleSpecificationCreate lcmRuleCreate = LCMRuleSpecificationCreateFromLCMRuleSpecification(lcmRule); // org.etsi.osl.tmf.lcm.model.LCMRuleSpecificationCreate.from
+                            ServiceSpecificationRef serviceSpecRef = createServiceSpecificationRef(newServSpec);
+                            List<ServiceSpecificationRef> serviceSpecs = new ArrayList<>();
+                            serviceSpecs.add(serviceSpecRef);
+                            lcmRuleCreate.setServiceSpecs(serviceSpecs);
+                            String jsonContent = JsonStringReplacer.replaceJsonStrings(objectMapper.writeValueAsString(lcmRuleCreate));
+                            String ruleUrl = apiEndpoint + "/lcmrulesmanagement/v1/lcmRuleSpecification";
+                            String token = keycloakAuthenticator.getToken();
+
+                            HttpResponse<String> response = sendHttpRequestWithRetry(ruleUrl, token, jsonContent, "POST", "post LCM Rule Specification", 3);
+
+                            if (response.statusCode() != 200) {
+                                logger.error("Failed to post LCM Rule Specification. HTTP status code: {}", response.statusCode());
+                                logger.error("Response body: {}", response.body());
+                            } else {
+                                logger.info("Successfully posted LCM Rule Specification. Response: {}", response.body());
+                            }
+                        } catch (IOException | InterruptedException e) {
+                            logger.error("Failed to process file: {} - {}", file, e.getMessage());
+                        }
+                    });
+        } catch (IOException e) {
+            logger.error("Failed to read directory: {} - {}", lcmRulesPath, e.getMessage());
+        }
+    }
+
+    private static ServiceSpecificationRef createServiceSpecificationRef(ServiceSpecification serviceSpec) {
+        ServiceSpecificationRef serviceSpecRef = new ServiceSpecificationRef();
+        serviceSpecRef.setId(serviceSpec.getId());
+        serviceSpecRef.setHref(serviceSpec.getHref());
+        serviceSpecRef.setName(serviceSpec.getName());
+        serviceSpecRef.setVersion(serviceSpec.getVersion());
+        serviceSpecRef.setBaseType(serviceSpec.getBaseType());
+        serviceSpecRef.setSchemaLocation(serviceSpec.getSchemaLocation());
+        serviceSpecRef.setType(serviceSpec.getType());
+        return serviceSpecRef;
+    }
+
+    // Static method to create LCMRuleSpecificationCreate from LCMRuleSpecification
+    public static LCMRuleSpecificationCreate LCMRuleSpecificationCreateFromLCMRuleSpecification(LCMRuleSpecification spec) {
+        LCMRuleSpecificationCreate create = new LCMRuleSpecificationCreate();
+        create.setName(spec.getName());
+        create.setDescription(spec.getDescription());
+        create.setPriority(spec.getPriority());
+        create.setLcmrulephase(spec.getLcmrulephase());
+        //create.setServiceSpecs((List<ServiceSpecificationRef>) spec.getServiceSpecs());
+        create.setContent(spec.getContent());
+        create.setCode(spec.getCode());
+        create.setLifecycleStatus(spec.getLifecycleStatus());
+        create.setVersion(spec.getVersion());
+        create.setValidFor(spec.getValidFor());
+        create.setBaseType(spec.getBaseType());
+        create.setSchemaLocation(spec.getSchemaLocation());
+        create.setType(spec.getType());
+        return create;
+    }
+
+    private static ServiceSpecification postServiceSpecMainProperties(ServiceSpecification servSpecFromFile) throws IOException, InterruptedException {
+        ServiceSpecificationCreate newServSpec = ServiceSpecificationPayloadsMaker.createServiceSpecDataFromFile(servSpecFromFile);
+        String jsonContent = JsonStringReplacer.replaceJsonStrings(objectMapper.writeValueAsString(newServSpec));
+        String ruleUrl = apiEndpoint + "/serviceCatalogManagement/v4/serviceSpecification";
+        String token = keycloakAuthenticator.getToken();
+
+        HttpResponse<String> response = sendHttpRequestWithRetry(ruleUrl, token, jsonContent, "POST", "post Service Specification", 3);
+
+        if (response.statusCode() != 200) {
+            throw new IOException("Failed request with status code: " + response.statusCode());
+        } else {
+            ServiceSpecification newServSpecTmp = objectMapper.readValue(response.body(), ServiceSpecification.class);
+            logger.info("ServiceSpecification created with UUID: {}", newServSpecTmp.getUuid());
+            return newServSpecTmp;
+        }
+    }
+
+    private static ResourceSpecification postResourceSpecMainProperties(ResourceSpecification resourceSpecFromFile) throws IOException, InterruptedException {
+        ResourceSpecificationCreate newResourceSpec = ResourceSpecificationPayloadsMaker.createResourceSpecDataFromFile(resourceSpecFromFile);
+        String jsonContent = JsonStringReplacer.replaceJsonStrings(objectMapper.writeValueAsString(newResourceSpec));
+        String ruleUrl = apiEndpoint + "/resourceCatalogManagement/v4/resourceSpecification";
+        String token = keycloakAuthenticator.getToken();
+
+        HttpResponse<String> response = sendHttpRequestWithRetry(ruleUrl, token, jsonContent, "POST", "post Resource Specification", 3);
+
+        if (response.statusCode() != 200) {
+            throw new IOException("Failed request with status code: " + response.statusCode());
+        } else {
+            logger.info("ResourceSpecification created");
+            JsonNode rootNode = objectMapper.readTree(response.body());
+            String baseType = rootNode.get("@type").asText();
+            if ("LogicalResourceSpecification".equals(baseType)) {
+                return objectMapper.treeToValue(rootNode, LogicalResourceSpecification.class);
+            } else if ("PhysicalResourceSpecification".equals(baseType)) {
+                return objectMapper.treeToValue(rootNode, PhysicalResourceSpecification.class);
+            } else {
+                throw new IllegalArgumentException("Unsupported ResourceSpecification baseType: " + baseType);
+            }
+        }
+    }
+
+    private static ServiceSpecification patchServiceSpecificationCharacteristics(ServiceSpecification servSpecFromFile, ServiceSpecification servSpecToPatch) throws IOException, InterruptedException {
+        ServiceSpecificationUpdate servSpecUpdate = ServiceSpecificationPayloadsMaker.updateServiceSpecDataFromFile(servSpecFromFile);
+        String jsonContent = JsonStringReplacer.replaceJsonStrings(objectMapper.writeValueAsString(servSpecUpdate));
+        String ruleUrl = apiEndpoint + "/serviceCatalogManagement/v4/serviceSpecification/" + servSpecToPatch.getUuid();
+        String token = keycloakAuthenticator.getToken();
+
+        HttpResponse<String> response = sendHttpRequestWithRetry(ruleUrl, token, jsonContent, "PATCH", "patch Service Specification", 3);
+
+        if (response.statusCode() != 200) {
+            throw new IOException("Failed request with status code: " + response.statusCode());
+        } else {
+            return objectMapper.readValue(response.body(), ServiceSpecification.class);
+        }
+    }
+
+    private static ResourceSpecification patchResourceSpecificationCharacteristics(ResourceSpecification resourceSpecFromFile, ResourceSpecification resourceSpecToPatch) throws IOException, InterruptedException {
+        ResourceSpecificationUpdate resourceSpecUpdate = ResourceSpecificationPayloadsMaker.updateResourceSpecDataFromFile(resourceSpecFromFile);
+        String jsonContent = JsonStringReplacer.replaceJsonStrings(objectMapper.writeValueAsString(resourceSpecUpdate));
+        String ruleUrl = apiEndpoint + "/resourceCatalogManagement/v4/resourceSpecification/" + resourceSpecToPatch.getUuid();
+        String token = keycloakAuthenticator.getToken();
+
+        HttpResponse<String> response = sendHttpRequestWithRetry(ruleUrl, token, jsonContent, "PATCH", "patch Service Specification", 3);
+
+        if (response.statusCode() != 200) {
+            throw new IOException("Failed request with status code: " + response.statusCode());
+        } else {
+            logger.info("response.body():"+response.body());
+            JsonNode rootNode = objectMapper.readTree(response.body());
+            String baseType = rootNode.get("@type").asText();
+            if ("LogicalResourceSpecification".equals(baseType)) {
+                return objectMapper.treeToValue(rootNode, LogicalResourceSpecification.class);
+            } else if ("PhysicalResourceSpecification".equals(baseType)) {
+                return objectMapper.treeToValue(rootNode, PhysicalResourceSpecification.class);
+            } else {
+                throw new IllegalArgumentException("Unsupported ResourceSpecification baseType: " + baseType);
+            }
+        }
+    }
+
+
+    private static void postResourceSpec(Path path, boolean isPhysical) {
+        try {
+            ResourceSpecification resourceSpecFromFile;
+            if (isPhysical) {
+                resourceSpecFromFile = objectMapper.readValue(path.toFile(), PhysicalResourceSpecification.class);
+            } else {
+                resourceSpecFromFile = objectMapper.readValue(path.toFile(), LogicalResourceSpecification.class);
+            }
+
+            // If the ServiceSpecification name and version already exists in the database, skip it to avoid duplicates
+            if (uuid2nameversion.containsValue(resourceSpecFromFile.getName() + " " + resourceSpecFromFile.getVersion())) {
+                logger.info("{} with name {} and version {} already exists. Skipping.",
+                        isPhysical ? "PhysicalResourceSpecification" : "LogicalResourceSpecification",
+                        resourceSpecFromFile.getName(), resourceSpecFromFile.getVersion());
+                return;
+            }
+
+            List<ResourceSpecification> resourceSpecificationsInOpenslice = getResourceSpecificationsInOpenslice();
+            for (ResourceSpecification resourceSpecificationInOpenSlice : resourceSpecificationsInOpenslice) {
+                if (resourceSpecificationInOpenSlice.getName().equals(resourceSpecFromFile.getName()) &&
+                        resourceSpecificationInOpenSlice.getVersion().equals(resourceSpecFromFile.getVersion())) {
+                    uuidmap.put(resourceSpecFromFile.getUuid(), resourceSpecificationInOpenSlice.getUuid());
+                    idmap.put(resourceSpecFromFile.getId(), resourceSpecificationInOpenSlice.getId());
+                    uuid2nameversion.put(resourceSpecificationInOpenSlice.getUuid(),
+                            resourceSpecificationInOpenSlice.getName() + " " + resourceSpecificationInOpenSlice.getVersion());
+                    logger.info("ResourceSpecification with name {} and version {} already exists. Skipping.",
+                            resourceSpecFromFile.getName(), resourceSpecFromFile.getVersion());
+                    return;
+                }
+            }
+
+            ResourceSpecification newResSpec = postResourceSpecMainProperties(resourceSpecFromFile);
+            uuidmap.put(resourceSpecFromFile.getUuid(), newResSpec.getUuid());
+            idmap.put(resourceSpecFromFile.getId(), newResSpec.getId());
+            uuid2nameversion.put(newResSpec.getUuid(), newResSpec.getName() + " " + newResSpec.getVersion());
+            newResSpec = patchResourceSpecificationCharacteristics(resourceSpecFromFile, newResSpec);
+            postResourceSpecificationAttachment(path, newResSpec);
+            newResSpec = patchResourceSpecificationResourceRelationships(resourceSpecFromFile, newResSpec);
+        } catch (IOException | InterruptedException e) {
+            logger.error("Process halted due to failure: {}", e.getMessage());
+        }
+    }
+
+    private static void postPhysicalResourceSpec(Path path) {
+        postResourceSpec(path, true);
+    }
+
+    private static void postLogicalResourceSpec(Path path) {
+        postResourceSpec(path, false);
+    }
+
+}
diff --git a/src/main/java/org/etsi/osl/util/mappings.yaml b/src/main/java/org/etsi/osl/util/mappings.yaml
new file mode 100644
index 0000000..33a3d08
--- /dev/null
+++ b/src/main/java/org/etsi/osl/util/mappings.yaml
@@ -0,0 +1,7 @@
+mappings:
+  - from: "string1"
+    to: "replacement1"
+  - from: "string2"
+    to: "replacement2"
+  - from: "string3"
+    to: "replacement3"
diff --git a/src/main/java/org/etsi/osl/util/mappings2.yaml b/src/main/java/org/etsi/osl/util/mappings2.yaml
new file mode 100644
index 0000000..89cc7c4
--- /dev/null
+++ b/src/main/java/org/etsi/osl/util/mappings2.yaml
@@ -0,0 +1,4 @@
+mappings:
+  "string1": "replacement1"
+  "string2": "replacement2"
+  "string3": "replacement3"
diff --git a/src/main/resources/Dockerfile b/src/main/resources/Dockerfile
new file mode 100644
index 0000000..a886b48
--- /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 servicespecificationuploader-0.0.1-SNAPSHOT.jar /app/servicespecificationuploader.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 /app/data-folder
+
+# Updated CMD to accept runtime arguments for data folder
+ENTRYPOINT ["sh", "-c", "java -jar /app/servicespecificationuploader.jar --configfile /app/config.properties --servicespecfolderpath $0"]
diff --git a/src/main/resources/config.properties b/src/main/resources/config.properties
new file mode 100644
index 0000000..b17d44c
--- /dev/null
+++ b/src/main/resources/config.properties
@@ -0,0 +1,6 @@
+keycloak.url=http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/token
+client.id=osapiWebClientId
+client.secret=admin
+username=admin
+password=admin
+targetApiEndpoint.url=http://localhost/tmf-api
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