src/core/config/ConfigReader.cpp

Implementation of config reader. More…

Detailed Description

Implementation of config reader.

Copyright: Copyright (c) 2017-2024 CERN and the Allpix Squared authors. This software is distributed under the terms of the MIT License, copied verbatim in the file “LICENSE.md”. In applying this license, CERN does not waive the privileges and immunities granted to it by virtue of its status as an Intergovernmental Organization or submit itself to any jurisdiction. SPDX-License-Identifier: MIT

Source code


#include "ConfigReader.hpp"

#include <algorithm>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>

#include "core/config/exceptions.h"
#include "core/utils/log.h"

using namespace allpix;

ConfigReader::ConfigReader() = default;
ConfigReader::ConfigReader(std::istream& stream, std::filesystem::path file_name) : ConfigReader() {
    add(stream, std::move(file_name));
}

ConfigReader::ConfigReader(const ConfigReader& other) : conf_array_(other.conf_array_) { copy_init_map(); }
ConfigReader& ConfigReader::operator=(const ConfigReader& other) {
    conf_array_ = other.conf_array_;
    copy_init_map();
    return *this;
}

void ConfigReader::copy_init_map() {
    conf_map_.clear();
    for(auto iter = conf_array_.begin(); iter != conf_array_.end(); ++iter) {
        conf_map_[allpix::transform(iter->getName(), ::tolower)].push_back(iter);
    }
}

std::pair<std::string, std::string> ConfigReader::parseKeyValue(std::string line) {
    line = allpix::trim(line);
    size_t equals_pos = line.find('=');
    if(equals_pos != std::string::npos) {
        std::string key = trim(std::string(line, 0, equals_pos));
        std::string value = trim(std::string(line, equals_pos + 1));
        char last_quote = 0;
        for(size_t i = 0; i < value.size(); ++i) {
            if(value[i] == '\'' || value[i] == '\"') {
                if(last_quote == 0) {
                    last_quote = value[i];
                } else if(last_quote == value[i]) {
                    last_quote = 0;
                }
            }
            if(last_quote == 0 && value[i] == '#') {
                value = std::string(value, 0, i);
                break;
            }
        }

        // Check if key contains only alphanumeric or underscores
        bool valid_key = true;
        for(auto& ch : key) {
            if(isalnum(ch) == 0 && ch != '_' && ch != '.' && ch != ':') {
                valid_key = false;
                break;
            }
        }

        // Check if value is not empty and key is valid
        if(!valid_key) {
            throw KeyValueParseError(line, "key is not valid");
        }
        if(value.empty()) {
            throw KeyValueParseError(line, "value is empty");
        }

        return std::make_pair(key, allpix::trim(value));
    }
    // Key / value pair does not contain equal sign
    throw KeyValueParseError(line, "missing equality sign to split key and value");
}

void ConfigReader::add(std::istream& stream, std::filesystem::path file_name) {
    LOG(TRACE) << "Parsing configuration file " << file_name;

    // Convert file name to absolute path (if given)
    if(!file_name.empty()) {
        file_name = std::filesystem::canonical(file_name);
    }

    // Build first empty configuration
    std::string section_name;
    Configuration conf(section_name, file_name);

    int line_num = 0;
    while(true) {
        // Process config line by line
        std::string line;
        if(stream.eof()) {
            break;
        }
        std::getline(stream, line);
        ++line_num;

        // Trim whitespaces at beginning and end of line:
        line = allpix::trim(line);

        // Ignore empty lines or comments
        if(line.empty() || line.front() == '#') {
            continue;
        }

        // Check if section header or key-value pair
        if(line.front() == '[') {
            // Line should be a section header with an alphanumeric name
            size_t idx = 1;
            for(; idx < line.length() - 1; ++idx) {
                if(isalnum(line[idx]) == 0 && line[idx] != '_') {
                    break;
                }
            }
            std::string remain = allpix::trim(line.substr(idx + 1));
            if(line[idx] == ']' && (remain.empty() || remain.front() == '#')) {
                // Ignore empty sections if they contain no configurations
                if(!conf.getName().empty() || conf.countSettings() > 0) {
                    // Add previous section
                    addConfiguration(std::move(conf));
                }

                // Begin new section
                section_name = std::string(line, 1, idx - 1);
                conf = Configuration(section_name, file_name);
            } else {
                // Section header is not valid
                throw ConfigParseError(file_name, line_num);
            }
        } else if(isalpha(line.front()) != 0) {
            // Line should be a key / value pair with an equal sign
            try {
                // Parse the key value pair
                auto [key, value] = parseKeyValue(std::move(line));

                // Add the config key
                conf.setText(key, value);
            } catch(KeyValueParseError& e) {
                // Rethrow key / value parse error as a configuration parse error
                throw ConfigParseError(file_name, line_num);
            }
        } else {
            // Line is not a comment, key/value pair or section header
            throw ConfigParseError(file_name, line_num);
        }
    }
    // Add last section
    addConfiguration(std::move(conf));
}

void ConfigReader::addConfiguration(Configuration config) {
    conf_array_.push_back(std::move(config));

    auto section_name = allpix::transform(conf_array_.back().getName(), ::tolower);
    conf_map_[section_name].push_back(--conf_array_.end());
}

void ConfigReader::clear() {
    conf_map_.clear();
    conf_array_.clear();
}

bool ConfigReader::hasConfiguration(std::string name) const {
    std::transform(name.begin(), name.end(), name.begin(), ::tolower);
    return conf_map_.find(name) != conf_map_.end();
}

unsigned int ConfigReader::countConfigurations(std::string name) const {
    std::transform(name.begin(), name.end(), name.begin(), ::tolower);
    if(!hasConfiguration(name)) {
        return 0;
    }
    return static_cast<unsigned int>(conf_map_.at(name).size());
}

Configuration ConfigReader::getHeaderConfiguration() const {
    // Get empty configurations
    std::vector<Configuration> configurations = getConfigurations("");
    if(configurations.empty()) {
        // Use all configurations to find the file name if no empty
        configurations = getConfigurations();
        std::string file_name;
        if(!configurations.empty()) {
            file_name = configurations.at(0).getFilePath();
        }
        return Configuration("", file_name);
    }

    // Merge all configurations
    Configuration header_config = configurations.at(0);
    for(auto& config : configurations) {
        // NOTE: Merging first configuration again has no effect
        header_config.merge(config);
    }
    return header_config;
}

std::vector<Configuration> ConfigReader::getConfigurations(std::string name) const {
    std::transform(name.begin(), name.end(), name.begin(), ::tolower);
    if(!hasConfiguration(name)) {
        return {};
    }

    std::vector<Configuration> result;
    for(const auto& iter : conf_map_.at(name)) {
        result.push_back(*iter);
    }
    return result;
}

std::vector<Configuration> ConfigReader::getConfigurations() const { return {conf_array_.begin(), conf_array_.end()}; }

Updated on 2024-12-13 at 08:31:37 +0000