Commit 5cd86cf3 authored by Ioannis Chatzis's avatar Ioannis Chatzis
Browse files

Implementation Commit. Closes #1

parent 6b979865
Loading
Loading
Loading
Loading

pom.xml

0 → 100644
+100 −0
Original line number Diff line number Diff line
<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>  
+197 −0
Original line number Diff line number Diff line
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);
        }
    }
}
+175 −0
Original line number Diff line number Diff line
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);
        }
    }
}
+60 −0
Original line number Diff line number Diff line
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
Loading