UCSDht.java

package it.cnr.iit.ucsdht;

import com.google.gson.GsonBuilder;
import com.google.gson.typeadapters.RuntimeTypeAdapterFactory;
import com.j256.ormlite.jdbc.JdbcConnectionSource;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
import it.cnr.iit.ucs.constants.STATUS;
import it.cnr.iit.ucs.pipreader.PIPReader;
import it.cnr.iit.ucs.properties.components.PepProperties;
import it.cnr.iit.ucs.properties.components.PipProperties;
import it.cnr.iit.ucs.sessionmanager.OnGoingAttribute;
import it.cnr.iit.ucs.sessionmanager.Session;
import it.cnr.iit.ucs.sessionmanager.SessionInterface;
import it.cnr.iit.ucs.sessionmanager.SessionManager;
import it.cnr.iit.ucsdht.json.Status;
import it.cnr.iit.ucsdht.json.StatusPersistent;
import it.cnr.iit.ucsdht.json.StatusRequestPostTopicUuid;
import it.cnr.iit.ucsdht.properties.*;
import it.cnr.iit.utility.JsonUtility;
import it.cnr.iit.utility.dht.DHTClient;
import it.cnr.iit.utility.dht.DHTPersistentMessageClient;
import it.cnr.iit.utility.dht.PersistUtility;
import it.cnr.iit.utility.dht.jsonpersistent.*;
import it.cnr.iit.xacml.Attribute;
import it.cnr.iit.xacml.Category;
import it.cnr.iit.xacml.DataType;
import it.cnr.iit.xacml.PolicyTags;
import it.cnr.iit.xacml.wrappers.PolicyWrapper;
import it.cnr.iit.xacml.wrappers.RequestWrapper;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;

import static it.cnr.iit.utility.dht.DHTUtils.isDhtReachable;

public class UCSDht {
    static DHTClient dhtClientEndPoint;
    static UCSClient ucsClient;

    static final String COMMAND_TYPE = "ucs-command";
    static final String UCS_SUB_TOPIC_UUID = "topic-uuid-the-ucs-is-subscribed-to";

    static final String PAP_SUB_TOPIC_NAME = "topic-name-pap-is-subscribed-to";
    static final String PAP_SUB_TOPIC_UUID = "topic-uuid-pap-is-subscribed-to";

    static final String PIP_SUB_TOPIC_NAME = "topic-name-pip-is-subscribed-to";
    static final String PIP_SUB_TOPIC_UUID = "topic-uuid-pip-is-subscribed-to";
    static final File pipsDir = new File(Utils.getResourcePath(UCSDht.class), "pips");
    static final File policiesDir = new File(Utils.getResourcePath(UCSDht.class), "policies");
    static final File pepsDir = new File(Utils.getResourcePath(UCSDht.class), "peps");
    static final File databaseDir = new File(Utils.getResourcePath(UCSDht.class), "database");

    private static String dhtUri = "ws://localhost:3000/ws";
    private static final String dbUri = "jdbc:sqlite:" + databaseDir + File.separator + "database.db";


    static final List<PipProperties> pipPropertiesList = new ArrayList<>();
    static final UCSDhtPapProperties papProperties = new UCSDhtPapProperties(policiesDir.getAbsolutePath());
    static final UCSDhtSessionManagerProperties sessionManagerProperties = new UCSDhtSessionManagerProperties();
    static final List<PepProperties> pepPropertiesList = new ArrayList<>();

    static List<SessionInterface> sessionsWithStatusStartOrRevoke = new ArrayList<>();

    static boolean softReset = false;
    static boolean hardReset = false;

    static String PERSISTENT_TOPIC_NAME_UCS = "SIFIS:UCS";
    static String PERSISTENT_TOPIC_UUID_UCS_STATUS = "status";
    static DHTPersistentMessageClient client;

    private static final RuntimeTypeAdapterFactory<RequestPostTopicUuid> typeFactory = RuntimeTypeAdapterFactory
            .of(RequestPostTopicUuid.class, "topic_name")
            .registerSubtype(StatusRequestPostTopicUuid.class, PERSISTENT_TOPIC_NAME_UCS);

    private static final RuntimeTypeAdapterFactory<Persistent> persistentTypeFactory = RuntimeTypeAdapterFactory
            .of(Persistent.class, "topic_name")
            .registerSubtype(StatusPersistent.class, PERSISTENT_TOPIC_NAME_UCS);

    public static void main(String[] args) {

        System.out.println(java.time.LocalDateTime.now() + " Start usage control engine...");

        for (int i = 0; i < args.length; i++) {
            switch (args[i]) {
                case "--help":
                    printUsage();
                    System.exit(0);
                case "--dht":
                    URI parsed = null;
                    try {
                        parsed = new URI(args[i + 1]);
                    } catch (URISyntaxException | ArrayIndexOutOfBoundsException e) {
                        // No URI indicated
                        System.err.println("Invalid URI after --dht option\n");
                        printUsage();
                        System.exit(1);
                    }
                    dhtUri = parsed.toString();
                    i = i + 1;
                    break;
                case "--soft-reset":
                    // reset the SQL database only
                    softReset = true;
                    break;
                case "--hard-reset":
                    // reset the SQL database and remove all PIPs, PEPs, and policies
                    hardReset = true;
                    break;
                default:
                    System.err.println("Unknown option " + args[i] + "\n");
                    printUsage();
                    System.exit(1);
            }
        }

        client = new DHTPersistentMessageClient(dhtUri, PERSISTENT_TOPIC_NAME_UCS,
                PERSISTENT_TOPIC_UUID_UCS_STATUS, typeFactory, persistentTypeFactory);

        if (hardReset) {
            performHardReset();
        } else if (softReset) {
            performSoftReset();
        } else {
            reloadState();
        }

        try {
            initializeUCS();
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }

        System.out.println();
        System.out.println("UCS initialization complete");
        System.out.println("  Database directory: " + databaseDir.getAbsolutePath());
        System.out.println("  PIPs directory: " + pipsDir.getAbsolutePath());
        System.out.println("  PEPs directory: " + pepsDir.getAbsolutePath());
        System.out.println("  Policies directory: " + policiesDir.getAbsolutePath());
        System.out.println();

        try {
            dhtClientEndPoint = new DHTClient(new URI(dhtUri));
            dhtClientEndPoint.addMessageHandler(new DhtMessageHandler());
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        StatusWatcher watcher = null;
        try {
            watcher = new StatusWatcher(Arrays.asList(
                    databaseDir.toPath(), pipsDir.toPath(), pepsDir.toPath(), policiesDir.toPath()));
            watcher.startMonitoring();
        } catch (IOException e) {
            if (watcher != null) {
                watcher.stopMonitoring();
            }
            throw new RuntimeException(e);
        }

        try {
            restorePIPsSubscriptions();
            reevaluateSessions();
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }

        System.out.println();
        System.out.println(java.time.LocalDateTime.now() + " Waiting for commands...");
        System.out.println();
    }

    public static void printUsage() {
        System.out.println(
                "Usage Control Engine\n" +
                "\n" +
                "Options:\n" +
                "--dht <dhtUri> The websocket URI of the DHT  \n" +
                "               (default: ws://localhost:3000/ws)\n" +
                "\n" +
                "--hard-reset   The UCS is initialized with a new database,\n" +
                "               all PIPs, PEPs, and policies are deleted.\n" +
                "               The new state is saved on the DHT right after \n" +
                "               the initialization.\n" +
                "\n" +
                "--soft-reset   The UCS is initialized with a new database,\n" +
                "               but all PIPs, PEPs, and policies are preserved.\n" +
                "               The new state is saved on the DHT right after \n" +
                "               the initialization.\n" +
                "\n" +
                "               If both --hard-reset and --soft-reset are specified,\n" +
                "               --hard-reset prevails.");

    }

    public static void performHardReset() {
        System.out.println(java.time.LocalDateTime.now() + " Performing hard reset ...");
        // reset everything
        initializeDb();
        Utils.createDir(pipsDir);
        Utils.createDir(pepsDir);
        Utils.createDir(policiesDir);
        System.out.println(java.time.LocalDateTime.now() + " ... hard reset performed");

        // save the new state to the dht
        uploadStatus();
    }

    public static void performSoftReset() {
        System.out.println(java.time.LocalDateTime.now() + " Performing soft reset ...");
        // get the state from the dht
        Status status = downloadStatus();
        if (status == null) {
            System.out.print("No status found: ");
            // if there is no state
            //   perform hard reset
            performHardReset();
        } else {
            System.out.println(java.time.LocalDateTime.now() + " ... status downloaded");
            initializeDb();
            restorePips(status.getPips());
            restorePeps(status.getPeps());
            restorePolicies(status.getPolicies());
            System.out.println(java.time.LocalDateTime.now() + " ... soft reset performed");

            // save the new state to the dht
            uploadStatus();
        }
    }

    public static void reloadState() {
        System.out.println(java.time.LocalDateTime.now() + " Reloading state ...");
        // get the state from the dht
        Status status = downloadStatus();
        if (status == null) {
            System.out.print("No status found: ");
            // if there is no state
            //   perform hard reset
            performHardReset();
        } else {
            System.out.println(java.time.LocalDateTime.now() + " ... status downloaded");
            restoreDb(status.getDatabase());
            restorePips(status.getPips());
            restorePeps(status.getPeps());
            restorePolicies(status.getPolicies());
            System.out.println(java.time.LocalDateTime.now() + " ... status reloaded");

            // do not save the new state to the dht since nothing changed
        }
    }


    /**
     * Create a new database file with empty tables
     */
    public static void initializeDb() {
        Utils.createDir(databaseDir);
        try (ConnectionSource connectionSource = new JdbcConnectionSource(dbUri)) {
            TableUtils.dropTable(connectionSource, Session.class, true);
            TableUtils.dropTable(connectionSource, OnGoingAttribute.class, true);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * Create the database file using the string obtained from the dht.
     * Then, extract the sessions with status START and REVOKED to be used after
     * the UCS is initialized in order to restore pips subscriptions.
     * @param dbString the base64 string representing the database file
     */
    public static void restoreDb(String dbString) {
        // write the database to file
        Utils.createDir(databaseDir);
        PersistUtility.base64StringToFile(dbString, databaseDir + File.separator + "database.db");
        System.out.println("[DATABASE] Database correctly retrieved from the DHT and saved to file");

        // save the sessions with status START and REVOKE
        UCSDhtSessionManagerProperties smProp = new UCSDhtSessionManagerProperties();
        smProp.setDbUri(dbUri);
        SessionManager sessionManager = new SessionManager(smProp);
        sessionManager.start();
        sessionsWithStatusStartOrRevoke.addAll(sessionManager.getSessionsForStatus(STATUS.START.toString()));
        sessionsWithStatusStartOrRevoke.addAll(sessionManager.getSessionsForStatus(STATUS.REVOKE.toString()));
        sessionManager.stop();
        System.out.println("[DATABASE] Sessions with status START and REVOKED gathered. "
                + sessionsWithStatusStartOrRevoke.size() + " sessions found.");
        System.out.println();
    }


    /**
     * Create the pip properties files using the strings obtained from the dht.
     * Then, load the pip properties in memory to be used during the UCS initialization.
     * @param pipsString the list of base64 strings representing the pip properties files
     */
    public static void restorePips(List<String> pipsString) {
        // write pip folders and files
        Utils.createDir(pipsDir);
        for (String pipString : pipsString) {
            // extract pip id
            PipProperties pipProperties =
                    PersistUtility.getPipPropertiesFromBase64String(pipString, UCSDhtPipProperties.class);
            assert pipProperties != null;
            String pipId = pipProperties.getId();

            // create pip properties json file
            Utils.createDir(new File(pipsDir.getAbsolutePath(), pipId));
            PersistUtility.base64StringToFile(pipString, pipsDir + File.separator + pipId + File.separator + pipId + ".json");
            System.out.println("[PIPs] PIP '" + pipId + "' correctly retrieved from the DHT and saved to file");

            // for PIPReader, create the file(s) with the attribute value
            if (pipProperties.getName().equals(PIPReader.class.getName())) {
                for (Map<String, String> attribute : pipProperties.getAttributes()) {
                    String attributeFilePath = pipProperties.getAdditionalProperties().get(attribute.get("ATTRIBUTE_ID"));
                    String attributeValue = pipProperties.getAdditionalProperties().get(attributeFilePath);
                    setAttributeValue(pipsDir.getAbsolutePath() + File.separator + pipId + File.separator + attributeFilePath, attributeValue);
                    System.out.println("        - Attribute value for '" + attribute.get("ATTRIBUTE_ID")
                            + "' correctly saved to file '" + attributeFilePath + "'");
                }
            }
        }
        System.out.println();

        // load the pips' properties in memory from the json files
        loadPips();
    }


    /**
     * Create the pep properties files using the strings obtained from the dht.
     * Then, load the pep properties in memory to be used during the UCS initialization.
     * @param pepsString the list of base64 strings representing the pep properties files
     */
    public static void restorePeps(List<String> pepsString) {
        // write pep files
        Utils.createDir(pepsDir);
        for (String pepString : pepsString) {
            // extract pep id
            PepProperties pepProperties = PersistUtility.getPepPropertiesFromBase64String(pepString, UCSDhtPepProperties.class);
            assert pepProperties != null;
            String pepId = pepProperties.getId();

            // create pep properties json file
            PersistUtility.base64StringToFile(pepString, pepsDir + File.separator + pepId + ".json");
            System.out.println("[PEPs] PEP '" + pepId + "' correctly retrieved from the DHT and saved to file");
        }
        System.out.println();

        // load the peps' properties in memory from the json files
        loadPeps();
    }


    /**
     * Create the policies files using the strings obtained from the dht.
     * @param policiesString the list of base64 strings representing the policy files
     */
    public static void restorePolicies(List<String> policiesString) {
        // write policies files
        Utils.createDir(policiesDir);
        for (String policyString : policiesString) {
            // extract policy id
            String policyId = PersistUtility.getPolicyIdFromBase64String(policyString);

            // create policy file
            PersistUtility.base64StringToFile(policyString, policiesDir + File.separator + policyId + ".xml");
            System.out.println("[Policies] Policy '" + policyId + "' correctly retrieved from the DHT and saved to file");
        }
        System.out.println();
    }


    /**
     * Transform the database file into a string in order to be stored in the dht.
     * @return a base64 string representing the database, or null if the file does
     * not exist
     */
    public static String saveDbToString() {
        File targetFile = new File(databaseDir, "database.db");
        if (targetFile.exists() && targetFile.isFile()) {
            return PersistUtility.fileToBase64String(databaseDir + File.separator + "database.db");
        } else {
            return null;
        }
    }

    /**
     * Transform the pip properties json files into strings in order to be stored in the dht
     * @return a list of base64 strings representing the pip properties files
     */
    public static List<String> savePipsToString() {

        List<String> pipStrings = new ArrayList<>();

        if (pipsDir.exists() && pipsDir.isDirectory()) {
            File[] subDirs = pipsDir.listFiles(File::isDirectory);
            if (subDirs != null) {
                for (File subDir : subDirs) {
                    File targetFile = new File(subDir, subDir.getName() + ".json");
                    if (targetFile.exists() && targetFile.isFile()) {
                        pipStrings.add(PersistUtility.fileToBase64String(
                                subDir.getAbsolutePath() + File.separator + subDir.getName() + ".json"));
                    } else {
                        System.out.println("Target file not found in subfolder: " + subDir.getAbsolutePath());
                    }
                }
            }
        } else {
            System.out.println("PIPs folder not found or is not a directory.");
        }
        return pipStrings;
    }


    /**
     * Transform the pep properties json files into strings in order to be stored in the dht
     * @return a list of base64 strings representing the pep properties files
     */
    public static List<String> savePepsToString() {

        List<String> pepStrings = new ArrayList<>();

        if (pepsDir.exists() && pepsDir.isDirectory()) {
            File[] files = pepsDir.listFiles(File::isFile);
            if (files != null) {
                for (File targetFile : files) {
                    pepStrings.add(PersistUtility.fileToBase64String(targetFile.getAbsolutePath()));
                }
            }
        } else {
            System.out.println("PEPs folder not found or is not a directory.");
        }
        return pepStrings;
    }


    /**
     * Transform the policies into strings in order to be stored in the dht
     * @return a list of base64 strings representing the policy files
     */
    public static List<String> savePoliciesToString() {

        List<String> policiesStrings = new ArrayList<>();

        if (policiesDir.exists() && policiesDir.isDirectory()) {
            File[] files = policiesDir.listFiles(File::isFile);
            if (files != null) {
                for (File targetFile : files) {
                    policiesStrings.add(PersistUtility.fileToBase64String(targetFile.getAbsolutePath()));
                }
            }
        } else {
            System.out.println("Policies folder not found or is not a directory.");
        }
        return policiesStrings;
    }

    /**
     * Obtain the UCS status from the dht.
     * @return the inner value field of the received response containing the base64 string
     * representations of the database, pips, peps, and policies
     */
    public static Status downloadStatus() {
        boolean isStatusExistent = true;

        System.out.println(java.time.LocalDateTime.now() + " Downloading status ...");
        // get the status from the dht
        String response = null;
        if (isDhtReachable(dhtUri, 2000, Integer.MAX_VALUE)) {
            RequestGetTopicUuid requestGetTopicUuid =
                    new RequestGetTopicUuid(PERSISTENT_TOPIC_NAME_UCS, PERSISTENT_TOPIC_UUID_UCS_STATUS);

            JsonOutRequestGetTopicUuid jsonOut = new JsonOutRequestGetTopicUuid(requestGetTopicUuid);
            String request = new GsonBuilder()
                    .disableHtmlEscaping()
                    .serializeNulls()
                    .create()
                    .toJson(jsonOut);

            response = client.sendRequestAndWaitForResponse(request);
            client.closeConnection();

            // If we get an empty response, perform the request
            // again to be sure that the empty response is actually
            // the response to our request.
            // If a response different from the empty response is
            // retrieved, exit the loop.
            if (response.equals("{\"Response\":{\"value\":{}}}")) {
                int attempts = 5;
                for (int i = 1; i <= attempts; i++) {
                    response = client.sendRequestAndWaitForResponse(request);
                    client.closeConnection();
                    if (!response.equals("{\"Response\":{\"value\":{}}}")) {
                        break;
                    }
                }
                isStatusExistent = false;
            }
        } else {
            System.exit(1);
        }

        if (!isStatusExistent) {
            return null;
        }

        JsonInResponse2RequestGetTopicUuid jsonInResponse = new GsonBuilder()
                .registerTypeAdapterFactory(typeFactory)
                .create().fromJson(response, JsonInResponse2RequestGetTopicUuid.class);

        StatusRequestPostTopicUuid statusRequestPostTopicUuid =
                (StatusRequestPostTopicUuid) jsonInResponse.getResponse().getValue();
        return statusRequestPostTopicUuid.getValue();
        //return jsonInResponse.getResponse().getValue().getValue();
    }


    /**
     * Save the UCS status to the dht
     */
    public static void uploadStatus() {
    System.out.println(java.time.LocalDateTime.now() + " Uploading status ...");
        Status value = new Status(
                saveDbToString(),
                savePipsToString(),
                savePepsToString(),
                savePoliciesToString());
        StatusRequestPostTopicUuid requestPostTopicUuid =
                new StatusRequestPostTopicUuid(value, PERSISTENT_TOPIC_UUID_UCS_STATUS);
        JsonOutRequestPostTopicUuid jsonOut = new JsonOutRequestPostTopicUuid(requestPostTopicUuid);
        String request = new GsonBuilder()
                .disableHtmlEscaping()
                .serializeNulls()
                .create()
                .toJson(jsonOut);

        String response = null;
        if (isDhtReachable(dhtUri, 2000, Integer.MAX_VALUE)) {
            response = client.sendRequestAndWaitForResponse(request);
            client.closeConnection();
        } else {
            System.exit(1);
        }

        try {
            JsonInPersistent jsonInPersistent = new GsonBuilder()
                    .registerTypeAdapterFactory(persistentTypeFactory)
                    .create().fromJson(response, JsonInPersistent.class);
        } catch (Exception e) {
            throw new RuntimeException("Unable to parse received Persistent message");
        }
            System.out.println(java.time.LocalDateTime.now() + " ... status uploaded");
    }


    /**
     * Add a sample PIP monitoring an environment attribute; then, initialize
     * the UCS; and, finally, add a sample policy.
     */
    private static void initializeUCS() {

        // to be correctly initialized, the UCS needs at least one PIP to be present
        if (pipPropertiesList.stream().noneMatch(id -> id.getId().equals("sample-pip"))) {
            addSamplePip("it.cnr.iit.ucs.pipreader.PIPReader", "sample-pip",
                    "urn:oasis:names:tc:xacml:3.0:environment:attribute-1",
                    Category.ENVIRONMENT.toString(), DataType.STRING.toString(), "sample-attribute-1.txt",
                    1000L, "attribute-1-value");
        }

        sessionManagerProperties.setDbUri(dbUri);

        // start the UCS
        ucsClient = new UCSClient(pipPropertiesList, papProperties, sessionManagerProperties, pepPropertiesList);

        // add sample policy
        String examplePolicy = Utils.readContent(Utils.accessFile(UCSDht.class, "example-policy.xml"));
        if (!ucsClient.addPolicy(examplePolicy)) {
            System.err.println("Failed to add policy");
        }
    }

    /**
     * Take the sessions with status START and REVOKE, and for each of them
     * get the list of attributes in the ongoing condition and the original
     * request. Then, call the subscribe method. This guarantees that a PIP
     * sets the correct additionalInformation in the attribute it subscribes
     * to.
     */
    public static void restorePIPsSubscriptions() {
        // restore PIPs' subscriptions
        for (SessionInterface session : sessionsWithStatusStartOrRevoke) {
            try {
                PolicyWrapper policy = PolicyWrapper.build(session.getPolicySet());
                List<Attribute> attributes = policy.getAttributesForCondition(PolicyTags.getCondition(STATUS.START));
                RequestWrapper request = RequestWrapper.build(session.getOriginalRequest(), ucsClient.getPipRegistry());
                ucsClient.getPipRegistry().subscribe(request.getRequestType(), attributes);
            } catch (Exception e) {
                System.err.println("Error restoring PIPs' subscriptions");
                throw new RuntimeException(e);
            }
        }
        System.out.println("[PIPs] PIPs' subscriptions restored");
    }

    /**
     * Reevaluate the sessions with status START and REVOKE. This has to be
     * done because the value of mutable attributes that led either to the
     * status START or REVOKE might have changed while the UCS was down, i.e.,
     * since the last status was saved to (and now loaded from) the dht and
     * the current time.
     * Possibly, some reevaluation will lead to a change of its status. If
     * this happens, a REEVALUATION message is sent to the interested pep.
     */
    public static void reevaluateSessions() {
        for (SessionInterface session : sessionsWithStatusStartOrRevoke) {
            try {
                ucsClient.getContextHandler().reevaluate(session);
            } catch (Exception e) {
                System.err.println("Error reevaluating sessions");
                throw new RuntimeException(e);
            }
        }
    }


    /**
     * Load the PIPs from json files. Each PIP is in a subfolder of the pipsDir
     * folder. The subfolder includes a json file containing the properties, and,
     * for PIPs of type PIPReader only, a file containing the attribute value.
     * |_ pipsDir
     *     |_ pip-id
     *         |_ pip-id.json
     *         |_ attribute-value.txt (only for PIPReader)
     * The json files contain the PipProperties that are added to the list
     * used to initialize the UCS.
     */
    public static void loadPips() {
        if (pipsDir.exists() && pipsDir.isDirectory()) {
            File[] subDirs = pipsDir.listFiles(File::isDirectory);
            if (subDirs != null) {
                for (File subDir : subDirs) {
                    File targetFile = new File(subDir, subDir.getName() + ".json");
                    if (targetFile.exists() && targetFile.isFile()) {
                        System.out.print("[PIPs] Loading PIP '" + targetFile.getName() + "' in memory ...");
                        Optional<UCSDhtPipProperties> properties = Optional.empty();
                        try {
                            properties = JsonUtility.loadObjectFromJsonFile(targetFile, UCSDhtPipProperties.class);
                        } catch (NoSuchElementException e) {
                            System.err.println(e.getMessage());
                        }
                        if (properties.isPresent()) {
                            if (properties.get().getName().equals(PIPReader.class.getName())) {
                                for (Map<String, String> attribute : properties.get().getAttributes()) {
                                    String attributeId = attribute.get("ATTRIBUTE_ID");
                                    String fileName = properties.get().getAdditionalProperties().get(attributeId);
                                    properties.get().getAdditionalProperties().put(attributeId,
                                            pipsDir + File.separator +
                                                    properties.get().getId() + File.separator + fileName);
                                }
                            }
                            pipPropertiesList.add(properties.get());
                        }
                        System.out.println(" [LOADED]");
                    } else {
                        System.out.println("Target file not found in subfolder: " + subDir.getAbsolutePath());
                    }
                }
            }
        } else {
            System.out.println("PIPs folder not found or is not a directory.");
        }
        System.out.println();
    }

    /**
     * Load the PEPs from json files within the pepsDir folder.
     * The json files contain the PepProperties that are added to the list
     * used to initialize the UCS.
     */
    public static void loadPeps() {
        if (pepsDir.exists() && pepsDir.isDirectory()) {
            File[] files = pepsDir.listFiles(File::isFile);
            if (files != null) {
                for (File targetFile : files) {
                    System.out.print("[PEPs] Loading PEP '" + targetFile.getName() + "' in memory ...");
                    Optional<UCSDhtPepProperties> properties = Optional.empty();
                    try {
                        properties = JsonUtility.loadObjectFromJsonFile(targetFile, UCSDhtPepProperties.class);
                    } catch (NoSuchElementException e) {
                        System.err.println(e.getMessage());
                    }
                    properties.ifPresent(pepPropertiesList::add);
                    System.out.println(" [LOADED]");
                }
            }
        } else {
            System.out.println("PEPs folder not found or is not a directory.");
        }
        System.out.println();
    }

    /**
     * Add a pip programmatically and save its json serialization and attribute file
     */
    public static void addSamplePip(String name, String pipId, String attributeId, String category, String dataType,
                                    String fileName, long refreshRate, String attributeValue) {

        UCSDhtPipProperties pipReader = new UCSDhtPipProperties();
        pipReader.setName(name);
        pipReader.addAttribute(attributeId, category, dataType);
        pipReader.setRefreshRate(refreshRate);
        pipReader.setJournalPath("/tmp/ucf");
        pipReader.setJournalProtocol("file");
        Map<String, String> additionalProperties = new HashMap<>();
        additionalProperties.put(attributeId, pipsDir + File.separator + pipId + File.separator + fileName);
        additionalProperties.put(pipsDir + File.separator + pipId + File.separator + fileName, attributeValue);
        pipReader.setAdditionalProperties(additionalProperties);
        pipPropertiesList.add(pipReader);

        Utils.createDir(new File(pipsDir.getAbsolutePath() + File.separator + pipId));

        setAttributeValue(pipsDir.getAbsolutePath() + File.separator + pipId
                + File.separator + fileName, attributeValue);

        // when serializing, we specify only the file name, not the entire path
        Map<String, String> additionalPropertiesToSerialize = new HashMap<>();
        additionalPropertiesToSerialize.put(attributeId, fileName);
        additionalPropertiesToSerialize.put(fileName, attributeValue);

        PIPMessageManager.serializePipToFile(name, pipId, attributeId, category,
                dataType, refreshRate, additionalPropertiesToSerialize);
    }

    /**
     * Write a value in a file
     *
     * @param fileName the name of the file
     * @param value    the value to write in the file
     */
    public static void setAttributeValue(String fileName, String value) {

        File file = new File(fileName);
        FileWriter fw = null;
        try {
            fw = new FileWriter(file);
            fw.write(value);
            fw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}