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