Ember
Loading...
Searching...
No Matches
ConfigManager.cpp
Go to the documentation of this file.
2#include "Utils/Logger.h"
3#include <cstdlib>
4#include <dirent.h>
5#include <fstream>
6#include <nlohmann/json.hpp>
7#include <pwd.h>
8#include <sys/stat.h>
9#include <sys/types.h>
10#include <unistd.h>
11
12namespace EmberCore {
13
14// Helper function to check if directory exists
15static bool DirectoryExists(const String &path) {
16 struct stat st;
17 return stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode);
18}
19
22
24 static ConfigManager instance;
25 return instance;
26}
27
29
30 // Find resources directory relative to the executable
31 // Check multiple candidate locations for flexibility
32 String resources_base;
33
34 // First check EMBER_RESOURCES environment variable
35 const char *env_resources = std::getenv("EMBER_RESOURCES");
36 if (env_resources && DirectoryExists(String(env_resources) + "/parser_profiles")) {
37 resources_base = String(env_resources);
38 } else {
39 // Get executable directory
40 char exe_path[1024];
41 ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
42
43 if (len != -1) {
44 exe_path[len] = '\0';
45 String exe_dir = String(exe_path);
46 size_t last_slash = exe_dir.find_last_of('/');
47 if (last_slash != String::npos) {
48 exe_dir = exe_dir.substr(0, last_slash);
49 }
50
51 // Try multiple candidate locations
52 std::vector<String> candidates = {
53 exe_dir + "/resources", // resources next to exe
54 exe_dir + "/../resources", // resources in parent (exe in bin/)
55 exe_dir + "/../../resources", // resources in grandparent (exe in build/bin/)
56 };
57
58 for (const auto &candidate : candidates) {
59 if (DirectoryExists(candidate + "/parser_profiles")) {
60 resources_base = candidate;
61 break;
62 }
63 }
64 }
65 }
66
67 // Set paths based on found resources directory, or fallback to relative
68 if (!resources_base.empty()) {
69 profiles_directory_ = resources_base + "/parser_profiles";
70 config_file_ = resources_base + "/parser_profiles/config.json";
71 } else {
72 // Fallback to relative path (CWD-based, not recommended but maintains compatibility)
73 profiles_directory_ = "resources/parser_profiles";
74 config_file_ = "resources/parser_profiles/config.json";
75 LOG_WARNING("ConfigManager", "Could not find resources directory, using relative path");
76 }
77
78 LOG_INFO("ConfigManager", "Profiles directory: " + profiles_directory_);
79 LOG_INFO("ConfigManager", "Config file: " + config_file_);
80
81 // Ensure directory exists and load profiles
84 }
85
86 // If no profiles exist, initialize defaults
87 if (profiles_.empty()) {
90 }
91
92 // Load config file (contains active profile and other settings)
94}
95
97 // Create parent directories if needed (resources, then parser_profiles)
98 String emberforge_dir = profiles_directory_.substr(0, profiles_directory_.find_last_of('/'));
99
100 struct stat st;
101 if (stat(emberforge_dir.c_str(), &st) != 0) {
102 if (mkdir(emberforge_dir.c_str(), 0755) != 0) {
103 LOG_ERROR("ConfigManager", "Failed to create .emberforge directory: " + emberforge_dir);
104 return false;
105 }
106 }
107
108 // Create parser_profiles directory
109 if (stat(profiles_directory_.c_str(), &st) != 0) {
110 if (mkdir(profiles_directory_.c_str(), 0755) != 0) {
111 LOG_ERROR("ConfigManager", "Failed to create profiles directory: " + profiles_directory_);
112 return false;
113 }
114 LOG_INFO("ConfigManager", "Created profiles directory: " + profiles_directory_);
115 }
116
117 return true;
118}
119
121 std::lock_guard<std::mutex> lock(mutex_);
123}
124
126 // Note: Mutex must be locked by caller
127 LOG_INFO("ConfigManager", "Initializing default profiles");
128
129 // Create default profile
130 auto default_profile = ParserProfile::CreateDefaultProfile();
131 profiles_[DEFAULT_PROFILE_NAME] = default_profile;
132
133 // Create generic profile
134 auto generic_profile = ParserProfile::CreateGenericProfile();
135 profiles_[GENERIC_PROFILE_NAME] = generic_profile;
136
138}
139
141 std::lock_guard<std::mutex> lock(mutex_);
142
143 LOG_INFO("ConfigManager", "Loading profiles from: " + profiles_directory_);
144
145 DIR *dir = opendir(profiles_directory_.c_str());
146 if (!dir) {
147 LOG_ERROR("ConfigManager", "Failed to open profiles directory");
148 return false;
149 }
150
151 int loaded_count = 0;
152 struct dirent *entry;
153 while ((entry = readdir(dir)) != nullptr) {
154 String filename = entry->d_name;
155
156 // Skip config.json (it's not a profile file)
157 if (filename == "config.json") {
158 continue;
159 }
160
161 // Only process .json files
162 if (filename.length() > 5 && filename.substr(filename.length() - 5) == ".json") {
163 String filepath = profiles_directory_ + "/" + filename;
164
165 auto profile = LoadProfileFromFile(filepath);
166 if (profile) {
167 profiles_[profile->GetName()] = profile;
168 loaded_count++;
169 }
170 }
171 }
172
173 closedir(dir);
174
175 LOG_INFO("ConfigManager", "Loaded " + std::to_string(loaded_count) + " profile(s)");
176 return true;
177}
178
179std::shared_ptr<ParserProfile> ConfigManager::LoadProfileFromFile(const String &filepath) {
180 try {
181 ParserProfile profile;
182 if (profile.LoadFromFile(filepath)) {
183 return std::make_shared<ParserProfile>(profile);
184 }
185 } catch (const std::exception &e) {
186 LOG_ERROR("ConfigManager", "Failed to load profile from " + filepath + ": " + e.what());
187 }
188 return nullptr;
189}
190
192 std::lock_guard<std::mutex> lock(mutex_);
193 return SaveProfilesInternal();
194}
195
197 // Note: Mutex must be locked by caller
199 return false;
200 }
201
202 LOG_INFO("ConfigManager", "Saving " + std::to_string(profiles_.size()) + " profile(s)");
203
204 bool all_saved = true;
205 for (const auto &pair : profiles_) {
206 const String &profile_name = pair.first;
207 if (!SaveProfile(profile_name)) {
208 all_saved = false;
209 }
210 }
211
212 // Save config file (includes active profile)
214
215 return all_saved;
216}
217
218bool ConfigManager::SaveProfile(const String &profile_name) {
219 auto profile = GetProfileInternal(profile_name);
220 if (!profile) {
221 LOG_ERROR("ConfigManager", "Profile not found: " + profile_name);
222 return false;
223 }
224
225 String filepath = GetProfileFilePath(profile_name);
226 return profile->SaveToFile(filepath);
227}
228
229bool ConfigManager::AddProfile(std::shared_ptr<ParserProfile> profile) {
230 if (!profile) {
231 LOG_ERROR("ConfigManager", "Cannot add null profile");
232 return false;
233 }
234
235 std::lock_guard<std::mutex> lock(mutex_);
236
237 String profile_name = profile->GetName();
238 if (HasProfileInternal(profile_name)) {
239 LOG_ERROR("ConfigManager", "Profile already exists: " + profile_name);
240 return false;
241 }
242
243 profiles_[profile_name] = profile;
244 SaveProfile(profile_name);
245
246 LOG_INFO("ConfigManager", "Added profile: " + profile_name);
247 return true;
248}
249
250bool ConfigManager::RemoveProfile(const String &profile_name) {
251 std::lock_guard<std::mutex> lock(mutex_);
252
253 // Don't allow removing default profiles
254 if (profile_name == DEFAULT_PROFILE_NAME || profile_name == GENERIC_PROFILE_NAME) {
255 LOG_ERROR("ConfigManager", "Cannot remove default profile: " + profile_name);
256 return false;
257 }
258
259 if (!HasProfileInternal(profile_name)) {
260 LOG_ERROR("ConfigManager", "Profile not found: " + profile_name);
261 return false;
262 }
263
264 // If this was the active profile, switch to default
265 if (active_profile_name_ == profile_name) {
267 }
268
269 // Remove from map
270 profiles_.erase(profile_name);
271
272 // Delete file
273 String filepath = GetProfileFilePath(profile_name);
274 if (unlink(filepath.c_str()) != 0) {
275 LOG_WARNING("ConfigManager", "Failed to delete profile file: " + filepath);
276 }
277
278 LOG_INFO("ConfigManager", "Removed profile: " + profile_name);
279 return true;
280}
281
282bool ConfigManager::RenameProfile(const String &old_name, const String &new_name) {
283 std::lock_guard<std::mutex> lock(mutex_);
284
285 // Don't allow renaming default profiles
286 if (old_name == DEFAULT_PROFILE_NAME || old_name == GENERIC_PROFILE_NAME) {
287 LOG_ERROR("ConfigManager", "Cannot rename default profile: " + old_name);
288 return false;
289 }
290
291 auto profile = GetProfileInternal(old_name);
292 if (!profile) {
293 LOG_ERROR("ConfigManager", "Profile not found: " + old_name);
294 return false;
295 }
296
297 if (HasProfileInternal(new_name)) {
298 LOG_ERROR("ConfigManager", "Profile with new name already exists: " + new_name);
299 return false;
300 }
301
302 // Update profile name
303 profile->SetName(new_name);
304 profile->UpdateModifiedTimestamp();
305
306 // Move in map
307 profiles_[new_name] = profile;
308 profiles_.erase(old_name);
309
310 // Update active profile name if needed
311 if (active_profile_name_ == old_name) {
312 active_profile_name_ = new_name;
313 }
314
315 // Delete old file and save with new name
316 String old_filepath = GetProfileFilePath(old_name);
317 unlink(old_filepath.c_str());
318 SaveProfile(new_name);
319
320 LOG_INFO("ConfigManager", "Renamed profile: " + old_name + " -> " + new_name);
321 return true;
322}
323
324std::shared_ptr<ParserProfile> ConfigManager::GetProfileInternal(const String &profile_name) const {
325 auto it = profiles_.find(profile_name);
326 if (it != profiles_.end()) {
327 return it->second;
328 }
329 return nullptr;
330}
331
332bool ConfigManager::HasProfileInternal(const String &profile_name) const {
333 return profiles_.find(profile_name) != profiles_.end();
334}
335
336std::shared_ptr<ParserProfile> ConfigManager::GetProfile(const String &profile_name) const {
337 std::lock_guard<std::mutex> lock(mutex_);
338 return GetProfileInternal(profile_name);
339}
340
341std::shared_ptr<ParserProfile> ConfigManager::GetActiveProfile() const { return GetProfile(active_profile_name_); }
342
343std::vector<String> ConfigManager::GetProfileNames() const {
344 std::lock_guard<std::mutex> lock(mutex_);
345
346 std::vector<String> names;
347 names.reserve(profiles_.size());
348
349 for (const auto &pair : profiles_) {
350 names.push_back(pair.first);
351 }
352
353 return names;
354}
355
356bool ConfigManager::HasProfile(const String &profile_name) const {
357 std::lock_guard<std::mutex> lock(mutex_);
358 return HasProfileInternal(profile_name);
359}
360
361bool ConfigManager::SetActiveProfile(const String &profile_name) {
362 std::lock_guard<std::mutex> lock(mutex_);
363
364 if (!HasProfileInternal(profile_name)) {
365 LOG_ERROR("ConfigManager", "Cannot set active profile, profile not found: " + profile_name);
366 return false;
367 }
368
369 active_profile_name_ = profile_name;
370
371 // Save config file with new active profile
373
374 LOG_INFO("ConfigManager", "Active profile set to: " + profile_name);
375 return true;
376}
377
378bool ConfigManager::ExportProfile(const String &profile_name, const String &filepath) {
379 std::lock_guard<std::mutex> lock(mutex_);
380
381 auto profile = GetProfileInternal(profile_name);
382 if (!profile) {
383 LOG_ERROR("ConfigManager", "Profile not found for export: " + profile_name);
384 return false;
385 }
386
387 return profile->SaveToFile(filepath);
388}
389
390bool ConfigManager::ImportProfile(const String &filepath, bool make_active) {
391 ParserProfile temp_profile;
392 if (!temp_profile.LoadFromFile(filepath)) {
393 LOG_ERROR("ConfigManager", "Failed to import profile from: " + filepath);
394 return false;
395 }
396
397 // Check for name conflicts and rename if necessary
398 String profile_name = temp_profile.GetName();
399 String original_name = profile_name;
400 int counter = 1;
401
402 while (HasProfile(profile_name)) {
403 profile_name = original_name + " (" + std::to_string(counter++) + ")";
404 }
405
406 if (profile_name != original_name) {
407 temp_profile.SetName(profile_name);
408 LOG_INFO("ConfigManager", "Renamed imported profile to avoid conflict: " + profile_name);
409 }
410
411 auto profile = std::make_shared<ParserProfile>(temp_profile);
412 if (!AddProfile(profile)) {
413 return false;
414 }
415
416 if (make_active) {
417 SetActiveProfile(profile_name);
418 }
419
420 return true;
421}
422
424 std::lock_guard<std::mutex> lock(mutex_);
425
426 LOG_INFO("ConfigManager", "Resetting to default profiles");
427
428 profiles_.clear();
429 InitializeDefaultProfilesInternal(); // Use internal version to avoid re-locking
430 SaveProfilesInternal(); // Use internal version to avoid re-locking
431}
432
434 // Create a safe filename from profile name
435 String safe_name = profile_name;
436 for (char &c : safe_name) {
437 if (!isalnum(c) && c != '_' && c != '-') {
438 c = '_';
439 }
440 }
441
442 return profiles_directory_ + "/" + safe_name + ".json";
443}
444
446 try {
447 std::ifstream file(config_file_);
448 if (!file.is_open()) {
449 LOG_INFO("ConfigManager", "Config file not found, creating with defaults: " + config_file_);
450 return SaveConfigFile(); // Create with current defaults
451 }
452
453 nlohmann::json json;
454 file >> json;
455 file.close();
456
457 // Load active profile name
458 if (json.contains("active_profile")) {
459 String loaded_profile = json["active_profile"].get<String>();
460
461 // Validate that the profile exists
462 if (HasProfile(loaded_profile)) {
463 active_profile_name_ = loaded_profile;
464 LOG_INFO("ConfigManager", "Loaded active profile: " + active_profile_name_);
465 } else {
466 LOG_WARNING("ConfigManager",
467 "Config file references non-existent profile '" + loaded_profile + "', using default");
469 }
470 }
471
472 return true;
473 } catch (const std::exception &e) {
474 LOG_ERROR("ConfigManager", "Failed to load config file: " + String(e.what()));
475 return false;
476 }
477}
478
480 try {
481 nlohmann::json json;
482
483 // Save active profile
484 json["active_profile"] = active_profile_name_;
485
486 // Add version for future compatibility
487 json["version"] = "1.0";
488
489 // Could add more settings here in the future:
490 // json["theme"] = "dark";
491 // json["last_opened_file"] = "...";
492 // etc.
493
494 std::ofstream file(config_file_);
495 if (!file.is_open()) {
496 LOG_ERROR("ConfigManager", "Failed to open config file for writing: " + config_file_);
497 return false;
498 }
499
500 file << std::setw(4) << json << std::endl;
501 file.close();
502
503 LOG_INFO("ConfigManager", "Saved config file: " + config_file_);
504 return true;
505 } catch (const std::exception &e) {
506 LOG_ERROR("ConfigManager", "Failed to save config file: " + String(e.what()));
507 return false;
508 }
509}
510
511} // namespace EmberCore
#define LOG_ERROR(category, message)
Definition Logger.h:116
#define LOG_WARNING(category, message)
Definition Logger.h:115
#define LOG_INFO(category, message)
Definition Logger.h:114
String GetProfileFilePath(const String &profile_name) const
std::shared_ptr< ParserProfile > GetActiveProfile() const
std::shared_ptr< ParserProfile > LoadProfileFromFile(const String &filepath)
bool SaveProfile(const String &profile_name)
bool SetActiveProfile(const String &profile_name)
static const String GENERIC_PROFILE_NAME
std::vector< String > GetProfileNames() const
std::shared_ptr< ParserProfile > GetProfileInternal(const String &profile_name) const
bool RemoveProfile(const String &profile_name)
bool ImportProfile(const String &filepath, bool make_active=false)
std::shared_ptr< ParserProfile > GetProfile(const String &profile_name) const
bool RenameProfile(const String &old_name, const String &new_name)
bool HasProfileInternal(const String &profile_name) const
bool AddProfile(std::shared_ptr< ParserProfile > profile)
static const String DEFAULT_PROFILE_NAME
std::map< String, std::shared_ptr< ParserProfile > > profiles_
bool HasProfile(const String &profile_name) const
ConfigManager(const ConfigManager &)=delete
bool ExportProfile(const String &profile_name, const String &filepath)
static ConfigManager & GetInstance()
A named parser configuration profile with metadata.
static std::shared_ptr< ParserProfile > CreateGenericProfile()
void SetName(const String &name)
static std::shared_ptr< ParserProfile > CreateDefaultProfile()
bool LoadFromFile(const String &filepath)
Main types header for EmberCore.
static bool DirectoryExists(const String &path)
std::string String
Framework-agnostic string type.
Definition String.h:14