KiCad PCB EDA Suite
json_settings.cpp
Go to the documentation of this file.
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 
21 #include <algorithm>
22 #include <fstream>
23 #include <iomanip>
24 #include <utility>
25 
26 #include <common.h>
27 #include <gal/color4d.h>
28 #include <settings/json_settings.h>
30 #include <settings/parameters.h>
31 #include <wx/config.h>
32 #include <wx/debug.h>
33 #include <wx/filename.h>
34 
35 extern const char* traceSettings;
36 
37 
38 JSON_SETTINGS::JSON_SETTINGS( const std::string& aFilename, SETTINGS_LOC aLocation,
39  int aSchemaVersion, bool aCreateIfMissing, bool aCreateIfDefault,
40  bool aWriteFile ) :
41  nlohmann::json(),
42  m_filename( aFilename ),
43  m_legacy_filename( "" ),
44  m_location( aLocation ),
45  m_createIfMissing( aCreateIfMissing ),
46  m_createIfDefault( aCreateIfDefault ),
47  m_writeFile( aWriteFile ),
48  m_deleteLegacyAfterMigration( true ),
49  m_resetParamsIfMissing( true ),
50  m_schemaVersion( aSchemaVersion ),
51  m_manager( nullptr )
52 {
53  ( *this )[PointerFromString( "meta.filename" )] = GetFullFilename();
54 
55  m_params.emplace_back(
56  new PARAM<int>( "meta.version", &m_schemaVersion, m_schemaVersion, true ) );
57 }
58 
59 
61 {
62  for( auto param: m_params )
63  delete param;
64 
65  m_params.clear();
66 }
67 
68 
70 {
71  return wxString( m_filename.c_str(), wxConvUTF8 ) + "." + getFileExt();
72 }
73 
74 
76 {
77  for( auto param : m_params )
78  {
79  try
80  {
81  param->Load( this, m_resetParamsIfMissing );
82  }
83  catch( ... )
84  {
85  // Skip unreadable parameters in file:
86 #ifdef DEBUG
87  wxLogMessage( wxString::Format( "param '%s' load err", param->GetJsonPath().c_str() ) );
88 #endif
89  }
90  }
91 }
92 
93 
94 bool JSON_SETTINGS::LoadFromFile( const std::string& aDirectory )
95 {
96  // First, load all params to default values
97  clear();
98  Load();
99 
100  bool success = true;
101  bool migrated = false;
102  bool legacy_migrated = false;
103 
104  LOCALE_IO locale;
105 
106  auto migrateFromLegacy = [&] ( wxFileName& aPath ) {
107  // Backup and restore during migration so that the original can be mutated if convenient
108  wxFileName temp;
109  temp.AssignTempFileName( aPath.GetFullPath() );
110 
111  bool backed_up = true;
112 
113  if( !wxCopyFile( aPath.GetFullPath(), temp.GetFullPath() ) )
114  {
115  wxLogTrace( traceSettings, "%s: could not create temp file for migration",
116  GetFullFilename() );
117  backed_up = false;
118  }
119 
120  wxConfigBase::DontCreateOnDemand();
121  auto cfg = std::make_unique<wxFileConfig>( wxT( "" ), wxT( "" ), aPath.GetFullPath() );
122 
123  // If migrate fails or is not implemented, fall back to built-in defaults that were
124  // already loaded above
125  if( !MigrateFromLegacy( cfg.get() ) )
126  {
127  wxLogTrace( traceSettings,
128  "%s: migrated; not all settings were found in legacy file",
129  GetFullFilename() );
130  }
131  else
132  {
133  wxLogTrace( traceSettings, "%s: migrated from legacy format", GetFullFilename() );
134  }
135 
136  if( backed_up )
137  {
138  cfg.reset();
139  wxCopyFile( temp.GetFullPath(), aPath.GetFullPath() );
140  wxRemoveFile( temp.GetFullPath() );
141  }
142 
143  // Either way, we want to clean up the old file afterwards
144  legacy_migrated = true;
145  };
146 
147  wxFileName path;
148 
149  if( aDirectory.empty() )
150  {
151  path.Assign( m_filename );
152  path.SetExt( getFileExt() );
153  }
154  else
155  {
156  wxString dir( aDirectory.c_str(), wxConvUTF8 );
157  path.Assign( dir, m_filename, getFileExt() );
158  }
159 
160  if( !path.Exists() )
161  {
162  // Case 1: legacy migration, no .json extension yet
163  path.SetExt( getLegacyFileExt() );
164 
165  if( path.Exists() )
166  {
167  migrateFromLegacy( path );
168  }
169  // Case 2: legacy filename is different from new one
170  else if( !m_legacy_filename.empty() )
171  {
172  path.SetName( m_legacy_filename );
173 
174  if( path.Exists() )
175  migrateFromLegacy( path );
176  }
177  else
178  {
179  success = false;
180  }
181  }
182  else
183  {
184  try
185  {
186  std::ifstream in( path.GetFullPath().ToUTF8() );
187  in >> *this;
188 
189  // If parse succeeds, check if schema migration is required
190  int filever = -1;
191 
192  try
193  {
194  filever = at( PointerFromString( "meta.version" ) ).get<int>();
195  }
196  catch( ... )
197  {
198  wxLogTrace(
199  traceSettings, "%s: file version could not be read!", GetFullFilename() );
200  success = false;
201  }
202 
203  if( filever >= 0 && filever < m_schemaVersion )
204  {
205  wxLogTrace( traceSettings, "%s: attempting migration from version %d to %d",
206  GetFullFilename(), filever, m_schemaVersion );
207 
208  if( Migrate() )
209  {
210  migrated = true;
211  }
212  else
213  {
214  wxLogTrace( traceSettings, "%s: migration failed!", GetFullFilename() );
215  }
216  }
217  else if( filever > m_schemaVersion )
218  {
219  wxLogTrace( traceSettings,
220  "%s: warning: file version %d is newer than latest (%d)", GetFullFilename(),
221  filever, m_schemaVersion );
222  }
223  }
224  catch( nlohmann::json::parse_error& error )
225  {
226  wxLogTrace(
227  traceSettings, "Parse error reading %s: %s", path.GetFullPath(), error.what() );
228  wxLogTrace( traceSettings, "Attempting migration in case file is in legacy format" );
229  migrateFromLegacy( path );
230  }
231  }
232 
233  // Now that we have new data in the JSON structure, load the params again
234  Load();
235 
236  // And finally load any nested settings
237  for( auto settings : m_nested_settings )
238  settings->LoadFromFile();
239 
240  wxLogTrace( traceSettings, "Loaded %s with schema %d", GetFullFilename(), m_schemaVersion );
241 
242  // If we migrated, clean up the legacy file (with no extension)
243  if( legacy_migrated || migrated )
244  {
245  if( legacy_migrated && m_deleteLegacyAfterMigration && !wxRemoveFile( path.GetFullPath() ) )
246  {
247  wxLogTrace(
248  traceSettings, "Warning: could not remove legacy file %s", path.GetFullPath() );
249  }
250 
251  // And write-out immediately so that we don't lose data if the program later crashes.
252  SaveToFile( aDirectory );
253  }
254 
255  return success;
256 }
257 
258 
260 {
261  bool modified = false;
262 
263  for( auto param : m_params )
264  {
265  modified |= !param->MatchesFile( this );
266  param->Store( this );
267  }
268 
269  return modified;
270 }
271 
272 
274 {
275  for( auto param : m_params )
276  param->SetDefault();
277 }
278 
279 
280 bool JSON_SETTINGS::SaveToFile( const std::string& aDirectory, bool aForce )
281 {
282  if( !m_writeFile )
283  return false;
284 
285  // Default PROJECT won't have a filename set
286  if( m_filename.empty() )
287  return false;
288 
289  wxFileName path;
290 
291  if( aDirectory.empty() )
292  {
293  path.Assign( m_filename );
294  path.SetExt( getFileExt() );
295  }
296  else
297  {
298  wxString dir( aDirectory.c_str(), wxConvUTF8 );
299  path.Assign( dir, m_filename, getFileExt() );
300  }
301 
302  if( !m_createIfMissing && !path.FileExists() )
303  {
304  wxLogTrace( traceSettings,
305  "File for %s doesn't exist and m_createIfMissing == false; not saving",
306  GetFullFilename() );
307  return false;
308  }
309 
310  bool modified = false;
311 
312  for( auto settings : m_nested_settings )
313  modified |= settings->SaveToFile();
314 
315  modified |= Store();
316 
317  if( !modified && !aForce && path.FileExists() )
318  {
319  wxLogTrace( traceSettings, "%s contents not modified, skipping save", GetFullFilename() );
320  return false;
321  }
322  else if( !modified && !aForce && !m_createIfDefault )
323  {
324  wxLogTrace( traceSettings,
325  "%s contents still default and m_createIfDefault == false; not saving",
326  GetFullFilename() );
327  return false;
328  }
329 
330  if( !path.DirExists() && !path.Mkdir() )
331  {
332  wxLogTrace( traceSettings, "Warning: could not create path %s, can't save %s",
333  path.GetPath(), GetFullFilename() );
334  return false;
335  }
336 
337  wxLogTrace( traceSettings, "Saving %s", GetFullFilename() );
338 
340 
341  try
342  {
343  std::ofstream file( path.GetFullPath().ToUTF8() );
344  file << std::setw( 2 ) << *this << std::endl;
345  }
346  catch( const std::exception& e )
347  {
348  wxLogTrace( traceSettings, "Warning: could not save %s: %s", GetFullFilename(), e.what() );
349  }
350  catch( ... )
351  {
352  }
353 
354  return true;
355 }
356 
357 
358 OPT<nlohmann::json> JSON_SETTINGS::GetJson( const std::string& aPath ) const
359 {
360  nlohmann::json::json_pointer ptr = PointerFromString( aPath );
361 
362  if( this->contains( ptr ) )
363  {
364  try
365  {
366  return OPT<nlohmann::json>{ this->at( ptr ) };
367  }
368  catch( ... )
369  {
370  }
371  }
372 
373  return OPT<nlohmann::json>{};
374 }
375 
376 
378 {
379  wxLogTrace( traceSettings, "Migrate() not implemented for %s", typeid( *this ).name() );
380  return false;
381 }
382 
383 
384 bool JSON_SETTINGS::MigrateFromLegacy( wxConfigBase* aLegacyConfig )
385 {
386  wxLogTrace( traceSettings,
387  "MigrateFromLegacy() not implemented for %s", typeid( *this ).name() );
388  return false;
389 }
390 
391 
392 nlohmann::json::json_pointer JSON_SETTINGS::PointerFromString( std::string aPath )
393 {
394  std::replace( aPath.begin(), aPath.end(), '.', '/' );
395  aPath.insert( 0, "/" );
396 
397  nlohmann::json::json_pointer p;
398 
399  try
400  {
401  p = nlohmann::json::json_pointer( aPath );
402  }
403  catch( ... )
404  {
405  wxASSERT_MSG( false, wxT( "Invalid pointer path in PointerFromString!" ) );
406  }
407 
408  return p;
409 }
410 
411 
412 template<typename ValueType>
413 bool JSON_SETTINGS::fromLegacy( wxConfigBase* aConfig, const std::string& aKey,
414  const std::string& aDest )
415 {
416  ValueType val;
417 
418  if( aConfig->Read( aKey, &val ) )
419  {
420  try
421  {
422  ( *this )[PointerFromString( aDest )] = val;
423  }
424  catch( ... )
425  {
426  wxASSERT_MSG( false, wxT( "Could not write value in fromLegacy!" ) );
427  return false;
428  }
429 
430  return true;
431  }
432 
433  return false;
434 }
435 
436 
437 // Explicitly declare these because we only support a few types anyway, and it means we can keep
438 // wxConfig detail out of the header file
439 template bool JSON_SETTINGS::fromLegacy<int>( wxConfigBase*, const std::string&,
440  const std::string& );
441 
442 template bool JSON_SETTINGS::fromLegacy<double>( wxConfigBase*, const std::string&,
443  const std::string& );
444 
445 template bool JSON_SETTINGS::fromLegacy<bool>( wxConfigBase*, const std::string&,
446  const std::string& );
447 
448 
449 bool JSON_SETTINGS::fromLegacyString( wxConfigBase* aConfig, const std::string& aKey,
450  const std::string& aDest )
451 {
452  wxString str;
453 
454  if( aConfig->Read( aKey, &str ) )
455  {
456  try
457  {
458  ( *this )[PointerFromString( aDest )] = str.ToUTF8();
459  }
460  catch( ... )
461  {
462  wxASSERT_MSG( false, wxT( "Could not write value in fromLegacyString!" ) );
463  return false;
464  }
465 
466  return true;
467  }
468 
469  return false;
470 }
471 
472 
473 bool JSON_SETTINGS::fromLegacyColor( wxConfigBase* aConfig, const std::string& aKey,
474  const std::string& aDest )
475 {
476  wxString str;
477 
478  if( aConfig->Read( aKey, &str ) )
479  {
481  color.SetFromWxString( str );
482 
483  try
484  {
485  nlohmann::json js = nlohmann::json::array( { color.r, color.g, color.b, color.a } );
486  ( *this )[PointerFromString( aDest )] = js;
487  }
488  catch( ... )
489  {
490  wxASSERT_MSG( false, wxT( "Could not write value in fromLegacyColor!" ) );
491  return false;
492  }
493 
494  return true;
495  }
496 
497  return false;
498 }
499 
500 
502 {
503  wxLogTrace( traceSettings, "AddNestedSettings %s", aSettings->GetFilename() );
504  m_nested_settings.push_back( aSettings );
505 }
506 
507 
509 {
510  if( !aSettings )
511  return;
512 
513  auto it = std::find_if( m_nested_settings.begin(), m_nested_settings.end(),
514  [&aSettings]( const JSON_SETTINGS* aPtr ) {
515  return aPtr == aSettings;
516  } );
517 
518  if( it != m_nested_settings.end() )
519  {
520  wxLogTrace( traceSettings, "Flush and release %s", ( *it )->GetFilename() );
521  ( *it )->SaveToFile();
522  m_nested_settings.erase( it );
523  }
524 
525  aSettings->SetParent( nullptr );
526 }
527 
528 
529 // Specializations to allow conversion between wxString and std::string via JSON_SETTINGS API
530 
531 template<> OPT<wxString> JSON_SETTINGS::Get( const std::string& aPath ) const
532 {
533  if( OPT<nlohmann::json> opt_json = GetJson( aPath ) )
534  return wxString( opt_json->get<std::string>().c_str(), wxConvUTF8 );
535 
536  return NULLOPT;
537 }
538 
539 
540 template<> void JSON_SETTINGS::Set<wxString>( const std::string& aPath, wxString aVal )
541 {
542  ( *this )[PointerFromString( std::move( aPath ) ) ] = aVal.ToUTF8();
543 }
544 
545 // Specializations to allow directly reading/writing wxStrings from JSON
546 
547 void to_json( nlohmann::json& aJson, const wxString& aString )
548 {
549  aJson = aString.ToUTF8();
550 }
551 
552 
553 void from_json( const nlohmann::json& aJson, wxString& aString )
554 {
555  aString = wxString( aJson.get<std::string>().c_str(), wxConvUTF8 );
556 }
void ResetToDefaults()
Resets all parameters to default values.
virtual bool Store()
Stores the current parameters into the JSON document represented by this object Note: this doesn't do...
std::string m_filename
The filename (not including path) of this settings file.
std::vector< PARAM_BASE * > m_params
The list of parameters (owned by this object)
Instantiate the current locale within a scope in which you are expecting exceptions to be thrown.
Definition: common.h:216
virtual bool SaveToFile(const std::string &aDirectory="", bool aForce=false)
bool m_createIfMissing
Whether or not the backing store file should be created it if doesn't exist.
virtual bool LoadFromFile(const std::string &aDirectory="")
Loads the backing file from disk and then calls Load()
int color
Definition: DXF_plotter.cpp:61
virtual wxString getLegacyFileExt() const
SETTINGS_LOC
Definition: json_settings.h:36
OPT< nlohmann::json > GetJson(const std::string &aPath) const
Fetches a JSON object that is a subset of this JSON_SETTINGS object, using a path of the form "key1....
wxString GetFullFilename() const
nlohmann::json json
Definition: gerbview.cpp:40
void from_json(const nlohmann::json &aJson, wxString &aString)
void AddNestedSettings(NESTED_SETTINGS *aSettings)
Transfers ownership of a given NESTED_SETTINGS to this object.
bool m_deleteLegacyAfterMigration
Whether or not to delete legacy file after migration.
OPT< ValueType > Get(const std::string &aPath) const
Fetches a value from within the JSON document.
virtual bool Migrate()
Migrates the schema of this settings from the version in the file to the latest version.
NESTED_SETTINGS is a JSON_SETTINGS that lives inside a JSON_SETTINGS.
const auto NULLOPT
Definition: optional.h:9
void to_json(nlohmann::json &aJson, const wxString &aString)
std::string m_legacy_filename
The filename of the wxConfig legacy file (if different from m_filename)
JSON_SETTINGS(const std::string &aFilename, SETTINGS_LOC aLocation, int aSchemaVersion)
Definition: json_settings.h:48
std::vector< NESTED_SETTINGS * > m_nested_settings
Nested settings files that live inside this one, if any.
bool fromLegacyString(wxConfigBase *aConfig, const std::string &aKey, const std::string &aDest)
Translates a legacy wxConfig string value to a given JSON pointer value.
bool m_resetParamsIfMissing
Whether or not to set parameters to their default value if missing from JSON on Load()
bool fromLegacyColor(wxConfigBase *aConfig, const std::string &aKey, const std::string &aDest)
Translates a legacy COLOR4D stored in a wxConfig string to a given JSON pointer value.
void Format(OUTPUTFORMATTER *out, int aNestLevel, int aCtl, CPTREE &aTree)
Function Format outputs a PTREE into s-expression format via an OUTPUTFORMATTER derivative.
Definition: ptree.cpp:205
static LIB_PART * dummy()
Used to draw a dummy shape when a LIB_PART is not found in library.
The common library.
boost::optional< T > OPT
Definition: optional.h:7
bool fromLegacy(wxConfigBase *aConfig, const std::string &aKey, const std::string &aDest)
Translates a legacy wxConfig value to a given JSON pointer value.
std::string GetFilename() const
Definition: json_settings.h:56
virtual ~JSON_SETTINGS()
bool m_createIfDefault
Whether or not the backing store file should be created if all parameters are still at their default ...
void ReleaseNestedSettings(NESTED_SETTINGS *aSettings)
Saves and frees a nested settings object, if it exists within this one.
const char * traceSettings
Flag to enable settings tracing.
virtual wxString getFileExt() const
static nlohmann::json::json_pointer PointerFromString(std::string aPath)
Builds a JSON pointer based on a given string.
virtual bool MigrateFromLegacy(wxConfigBase *aLegacyConfig)
Migrates from wxConfig to JSON-based configuration.
int m_schemaVersion
Version of this settings schema.
virtual void Load()
Updates the parameters of this object based on the current JSON document contents.
bool m_writeFile
Whether or not the backing store file should be written.
COLOR4D is the color representation with 4 components: red, green, blue, alpha.
Definition: color4d.h:99
void SetParent(JSON_SETTINGS *aParent)