You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

411 lines
11 KiB

  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2020 Jon Evans <jon@craftyjon.com>
  5. * Copyright (C) 2020 KiCad Developers, see AUTHORS.txt for contributors.
  6. *
  7. * This program is free software: you can redistribute it and/or modify it
  8. * under the terms of the GNU General Public License as published by the
  9. * Free Software Foundation, either version 3 of the License, or (at your
  10. * option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful, but
  13. * WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  15. * General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License along
  18. * with this program. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. #include <boost/algorithm/string.hpp>
  21. #include <boost/algorithm/string/replace.hpp>
  22. #include <fstream>
  23. #include <iomanip>
  24. #include <utility>
  25. #include <common.h>
  26. #include <gal/color4d.h>
  27. #include <settings/json_settings.h>
  28. #include <settings/nested_settings.h>
  29. #include <settings/parameters.h>
  30. #include <wx/config.h>
  31. #include <wx/debug.h>
  32. #include <wx/filename.h>
  33. extern const char* traceSettings;
  34. JSON_SETTINGS::JSON_SETTINGS( const std::string& aFilename, SETTINGS_LOC aLocation,
  35. int aSchemaVersion, bool aCreateIfMissing, bool aWriteFile,
  36. nlohmann::json aDefault ) :
  37. nlohmann::json( std::move( aDefault ) ), m_filename( aFilename ), m_legacy_filename( "" ),
  38. m_location( aLocation ), m_createIfMissing( aCreateIfMissing ), m_writeFile( aWriteFile ),
  39. m_schemaVersion( aSchemaVersion )
  40. {
  41. m_params.emplace_back(
  42. new PARAM<std::string>( "meta.filename", &m_filename, m_filename, true ) );
  43. m_params.emplace_back(
  44. new PARAM<int>( "meta.version", &m_schemaVersion, m_schemaVersion, true ) );
  45. }
  46. JSON_SETTINGS::~JSON_SETTINGS()
  47. {
  48. for( auto param: m_params )
  49. delete param;
  50. m_params.clear();
  51. }
  52. void JSON_SETTINGS::Load()
  53. {
  54. for( auto param : m_params )
  55. param->Load( this );
  56. }
  57. void JSON_SETTINGS::LoadFromFile( const std::string& aDirectory )
  58. {
  59. // First, load all params to default values
  60. clear();
  61. Load();
  62. bool migrated = false;
  63. LOCALE_IO locale;
  64. auto migrateFromLegacy = [&] ( wxFileName& aPath ) {
  65. wxConfigBase::DontCreateOnDemand();
  66. auto cfg = std::make_unique<wxFileConfig>( wxT( "" ), wxT( "" ), aPath.GetFullPath() );
  67. // If migrate fails or is not implemented, fall back to built-in defaults that were
  68. // already loaded above
  69. if( !MigrateFromLegacy( cfg.get() ) )
  70. {
  71. wxLogTrace( traceSettings,
  72. "%s: migrated; not all settings were found in legacy file", m_filename );
  73. }
  74. else
  75. {
  76. wxLogTrace( traceSettings, "%s: migrated from legacy format", m_filename );
  77. }
  78. // Either way, we want to clean up the old file afterwards
  79. migrated = true;
  80. };
  81. wxFileName path( aDirectory, m_filename, "json" );
  82. if( !path.Exists() )
  83. {
  84. // Case 1: legacy migration, no .json extension yet
  85. path.ClearExt();
  86. if( path.Exists() )
  87. {
  88. migrateFromLegacy( path );
  89. }
  90. // Case 2: legacy filename is different from new one
  91. else if( !m_legacy_filename.empty() )
  92. {
  93. path.SetName( m_legacy_filename );
  94. if( path.Exists() )
  95. migrateFromLegacy( path );
  96. }
  97. }
  98. else
  99. {
  100. try
  101. {
  102. std::ifstream in( path.GetFullPath().ToStdString() );
  103. in >> *this;
  104. // If parse succeeds, check if schema migration is required
  105. try
  106. {
  107. int filever = at( PointerFromString( "meta.version" ) ).get<int>();
  108. if( filever < m_schemaVersion )
  109. {
  110. wxLogTrace( traceSettings, "%s: attempting migration from version %d to %d",
  111. m_filename, filever, m_schemaVersion );
  112. if( !Migrate() )
  113. {
  114. wxLogTrace( traceSettings, "%s: migration failed!", m_filename );
  115. }
  116. }
  117. else if( filever > m_schemaVersion )
  118. {
  119. wxLogTrace( traceSettings,
  120. "%s: warning: file version %d is newer than latest (%d)", m_filename,
  121. filever, m_schemaVersion );
  122. }
  123. }
  124. catch( ... )
  125. {
  126. wxLogTrace( traceSettings, "%s: file version could not be read!", m_filename );
  127. }
  128. }
  129. catch( nlohmann::json::parse_error& error )
  130. {
  131. wxLogTrace(
  132. traceSettings, "Parse error reading %s: %s", path.GetFullPath(), error.what() );
  133. wxLogTrace( traceSettings, "Attempting migration in case file is in legacy format" );
  134. migrateFromLegacy( path );
  135. }
  136. }
  137. // Now that we have new data in the JSON structure, load the params again
  138. Load();
  139. // And finally load any nested settings
  140. for( auto settings : m_nested_settings )
  141. settings->LoadFromFile();
  142. wxLogTrace( traceSettings, "Loaded %s with schema %d", GetFilename(), m_schemaVersion );
  143. // If we migrated, clean up the legacy file (with no extension)
  144. if( migrated )
  145. {
  146. if( !wxRemoveFile( path.GetFullPath() ) )
  147. {
  148. wxLogTrace(
  149. traceSettings, "Warning: could not remove legacy file %s", path.GetFullPath() );
  150. }
  151. // And write-out immediately so that we don't lose data if the program later crashes.
  152. SaveToFile( aDirectory );
  153. }
  154. }
  155. void JSON_SETTINGS::Store()
  156. {
  157. for( auto param : m_params )
  158. param->Store( this );
  159. }
  160. void JSON_SETTINGS::ResetToDefaults()
  161. {
  162. for( auto param : m_params )
  163. param->SetDefault();
  164. }
  165. void JSON_SETTINGS::SaveToFile( const std::string& aDirectory )
  166. {
  167. if( !m_writeFile )
  168. return;
  169. wxLogTrace( traceSettings, "Saving %s", m_filename );
  170. wxFileName path( aDirectory, m_filename, "json" );
  171. if( !m_createIfMissing && !path.FileExists() )
  172. return;
  173. if( !path.DirExists() && !path.Mkdir() )
  174. {
  175. wxLogTrace( traceSettings, "Warning: could not create path %s, can't save %s",
  176. path.GetPath(), m_filename );
  177. return;
  178. }
  179. for( auto settings : m_nested_settings )
  180. settings->SaveToFile();
  181. Store();
  182. LOCALE_IO dummy;
  183. try
  184. {
  185. std::ofstream file( path.GetFullPath().ToStdString() );
  186. file << std::setw( 2 ) << *this << std::endl;
  187. }
  188. catch( const std::exception& e )
  189. {
  190. wxLogTrace( traceSettings, "Warning: could not save %s: %s", m_filename, e.what() );
  191. }
  192. catch( ... )
  193. {
  194. }
  195. }
  196. nlohmann::json JSON_SETTINGS::GetJson( std::string aPath ) const
  197. {
  198. nlohmann::json ret( {} );
  199. // Will throw an exception if the path is not found
  200. try
  201. {
  202. ret = this->at( PointerFromString( std::move( aPath ) ) );
  203. }
  204. catch( ... )
  205. {
  206. }
  207. return ret;
  208. }
  209. bool JSON_SETTINGS::Migrate()
  210. {
  211. wxLogTrace( traceSettings, "Migrate() not implemented for %s", typeid( *this ).name() );
  212. return false;
  213. }
  214. bool JSON_SETTINGS::MigrateFromLegacy( wxConfigBase* aLegacyConfig )
  215. {
  216. wxLogTrace( traceSettings,
  217. "MigrateFromLegacy() not implemented for %s", typeid( *this ).name() );
  218. return false;
  219. }
  220. nlohmann::json::json_pointer JSON_SETTINGS::PointerFromString( std::string aPath )
  221. {
  222. boost::replace_all( aPath, ".", "/" );
  223. aPath.insert( 0, "/" );
  224. nlohmann::json::json_pointer p;
  225. try
  226. {
  227. p = nlohmann::json::json_pointer( aPath );
  228. }
  229. catch( ... )
  230. {
  231. wxASSERT_MSG( false, wxT( "Invalid pointer path in PointerFromString!" ) );
  232. }
  233. return p;
  234. }
  235. template<typename ValueType>
  236. bool JSON_SETTINGS::fromLegacy( wxConfigBase* aConfig, const std::string& aKey,
  237. const std::string& aDest )
  238. {
  239. ValueType val;
  240. if( aConfig->Read( aKey, &val ) )
  241. {
  242. try
  243. {
  244. ( *this )[PointerFromString( aDest )] = val;
  245. }
  246. catch( ... )
  247. {
  248. wxASSERT_MSG( false, wxT( "Could not write value in fromLegacy!" ) );
  249. return false;
  250. }
  251. return true;
  252. }
  253. return false;
  254. }
  255. // Explicitly declare these because we only support a few types anyway, and it means we can keep
  256. // wxConfig detail out of the header file
  257. template bool JSON_SETTINGS::fromLegacy<int>( wxConfigBase*, const std::string&,
  258. const std::string& );
  259. template bool JSON_SETTINGS::fromLegacy<double>( wxConfigBase*, const std::string&,
  260. const std::string& );
  261. template bool JSON_SETTINGS::fromLegacy<bool>( wxConfigBase*, const std::string&,
  262. const std::string& );
  263. bool JSON_SETTINGS::fromLegacyString( wxConfigBase* aConfig, const std::string& aKey,
  264. const std::string& aDest )
  265. {
  266. wxString str;
  267. if( aConfig->Read( aKey, &str ) )
  268. {
  269. try
  270. {
  271. ( *this )[PointerFromString( aDest )] = str.ToUTF8();
  272. }
  273. catch( ... )
  274. {
  275. wxASSERT_MSG( false, wxT( "Could not write value in fromLegacyString!" ) );
  276. return false;
  277. }
  278. return true;
  279. }
  280. return false;
  281. }
  282. bool JSON_SETTINGS::fromLegacyColor( wxConfigBase* aConfig, const std::string& aKey,
  283. const std::string& aDest )
  284. {
  285. wxString str;
  286. if( aConfig->Read( aKey, &str ) )
  287. {
  288. KIGFX::COLOR4D color;
  289. color.SetFromWxString( str );
  290. try
  291. {
  292. nlohmann::json js = nlohmann::json::array( { color.r, color.g, color.b, color.a } );
  293. ( *this )[PointerFromString( aDest )] = js;
  294. }
  295. catch( ... )
  296. {
  297. wxASSERT_MSG( false, wxT( "Could not write value in fromLegacyColor!" ) );
  298. return false;
  299. }
  300. return true;
  301. }
  302. return false;
  303. }
  304. void JSON_SETTINGS::AddNestedSettings( NESTED_SETTINGS* aSettings )
  305. {
  306. m_nested_settings.push_back( aSettings );
  307. }
  308. // Specializations to allow conversion between wxString and std::string via JSON_SETTINGS API
  309. template<> wxString JSON_SETTINGS::Get( std::string aPath ) const
  310. {
  311. return wxString( GetJson( std::move( aPath ) ).get<std::string>().c_str(), wxConvUTF8 );
  312. }
  313. template<> void JSON_SETTINGS::Set<wxString>( std::string aPath, wxString aVal )
  314. {
  315. ( *this )[PointerFromString( std::move( aPath ) ) ] = aVal.ToUTF8();
  316. }
  317. // Specializations to allow directly reading/writing wxStrings from JSON
  318. void to_json( nlohmann::json& aJson, const wxString& aString )
  319. {
  320. aJson = aString.ToUTF8();
  321. }
  322. void from_json( const nlohmann::json& aJson, wxString& aString )
  323. {
  324. aString = wxString( aJson.get<std::string>().c_str(), wxConvUTF8 );
  325. }