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 #include <sstream>
26 
27 #include <common.h>
28 #include <gal/color4d.h>
29 #include <settings/json_settings.h>
31 #include <settings/parameters.h>
32 #include <wx/config.h>
33 #include <wx/debug.h>
34 #include <wx/filename.h>
35 
36 const wxChar* const traceSettings = wxT( "KICAD_SETTINGS" );
37 
38 JSON_SETTINGS::JSON_SETTINGS( const wxString& 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  try
54  {
55  ( *this )[PointerFromString( "meta.filename" )] = GetFullFilename();
56  }
57  catch( ... )
58  {
59  wxLogTrace( traceSettings, "Error: Could not create filename field for %s",
60  GetFullFilename() );
61  }
62 
63 
64  m_params.emplace_back(
65  new PARAM<int>( "meta.version", &m_schemaVersion, m_schemaVersion, true ) );
66 }
67 
68 
70 {
71  for( auto param: m_params )
72  delete param;
73 
74  m_params.clear();
75 }
76 
77 
79 {
80  return wxString( m_filename + "." + getFileExt() );
81 }
82 
83 
85 {
86  for( auto param : m_params )
87  {
88  try
89  {
90  param->Load( this, m_resetParamsIfMissing );
91  }
92  catch( ... )
93  {
94  // Skip unreadable parameters in file
95  wxLogTrace( traceSettings, "param '%s' load err", param->GetJsonPath().c_str() );
96  }
97  }
98 }
99 
100 
101 bool JSON_SETTINGS::LoadFromFile( const wxString& aDirectory )
102 {
103  // First, load all params to default values
104  clear();
105  Load();
106 
107  bool success = true;
108  bool migrated = false;
109  bool legacy_migrated = false;
110 
111  LOCALE_IO locale;
112 
113  auto migrateFromLegacy = [&] ( wxFileName& aPath ) {
114  // Backup and restore during migration so that the original can be mutated if convenient
115  bool backed_up = false;
116  wxFileName temp;
117 
118  if( aPath.IsDirWritable() )
119  {
120  temp.AssignTempFileName( aPath.GetFullPath() );
121 
122  if( !wxCopyFile( aPath.GetFullPath(), temp.GetFullPath() ) )
123  {
124  wxLogTrace( traceSettings, "%s: could not create temp file for migration",
125  GetFullFilename() );
126  }
127  else
128  backed_up = true;
129  }
130 
131  wxConfigBase::DontCreateOnDemand();
132  auto cfg = std::make_unique<wxFileConfig>( wxT( "" ), wxT( "" ), aPath.GetFullPath() );
133 
134  // If migrate fails or is not implemented, fall back to built-in defaults that were
135  // already loaded above
136  if( !MigrateFromLegacy( cfg.get() ) )
137  {
138  wxLogTrace( traceSettings,
139  "%s: migrated; not all settings were found in legacy file",
140  GetFullFilename() );
141  }
142  else
143  {
144  wxLogTrace( traceSettings, "%s: migrated from legacy format", GetFullFilename() );
145  }
146 
147  if( backed_up )
148  {
149  cfg.reset();
150 
151  if( !wxCopyFile( temp.GetFullPath(), aPath.GetFullPath() ) )
152  {
153  wxLogTrace( traceSettings,
154  "migrate; copy temp file %s to %s failed",
155  temp.GetFullPath(), aPath.GetFullPath() );
156  }
157 
158  if( !wxRemoveFile( temp.GetFullPath() ) )
159  {
160  wxLogTrace( traceSettings,
161  "migrate; failed to remove temp file %s",
162  temp.GetFullPath() );
163  }
164  }
165 
166  // Either way, we want to clean up the old file afterwards
167  legacy_migrated = true;
168  };
169 
170  wxFileName path;
171 
172  if( aDirectory.empty() )
173  {
174  path.Assign( m_filename );
175  path.SetExt( getFileExt() );
176  }
177  else
178  {
179  wxString dir( aDirectory );
180  path.Assign( dir, m_filename, getFileExt() );
181  }
182 
183  if( !path.Exists() )
184  {
185  // Case 1: legacy migration, no .json extension yet
186  path.SetExt( getLegacyFileExt() );
187 
188  if( path.Exists() )
189  {
190  migrateFromLegacy( path );
191  }
192  // Case 2: legacy filename is different from new one
193  else if( !m_legacy_filename.empty() )
194  {
195  path.SetName( m_legacy_filename );
196 
197  if( path.Exists() )
198  migrateFromLegacy( path );
199  }
200  else
201  {
202  success = false;
203  }
204  }
205  else
206  {
207  try
208  {
209  FILE* fp = wxFopen( path.GetFullPath(), wxT( "rt" ) );
210  *static_cast<nlohmann::json*>( this ) = nlohmann::json::parse( fp, nullptr,
211  /* allow_exceptions = */ true,
212  /* ignore_comments = */ true );
213 
214  // If parse succeeds, check if schema migration is required
215  int filever = -1;
216 
217  try
218  {
219  filever = at( PointerFromString( "meta.version" ) ).get<int>();
220  }
221  catch( ... )
222  {
223  wxLogTrace(
224  traceSettings, "%s: file version could not be read!", GetFullFilename() );
225  success = false;
226  }
227 
228  if( filever >= 0 && filever < m_schemaVersion )
229  {
230  wxLogTrace( traceSettings, "%s: attempting migration from version %d to %d",
231  GetFullFilename(), filever, m_schemaVersion );
232 
233  if( Migrate() )
234  {
235  migrated = true;
236  }
237  else
238  {
239  wxLogTrace( traceSettings, "%s: migration failed!", GetFullFilename() );
240  }
241  }
242  else if( filever > m_schemaVersion )
243  {
244  wxLogTrace( traceSettings,
245  "%s: warning: file version %d is newer than latest (%d)", GetFullFilename(),
246  filever, m_schemaVersion );
247  }
248  }
249  catch( nlohmann::json::parse_error& error )
250  {
251  wxLogTrace( traceSettings, "Json parse error reading %s: %s",
252  path.GetFullPath(), error.what() );
253  wxLogTrace( traceSettings, "Attempting migration in case file is in legacy format" );
254  migrateFromLegacy( path );
255  }
256  }
257 
258  // Now that we have new data in the JSON structure, load the params again
259  Load();
260 
261  // And finally load any nested settings
262  for( auto settings : m_nested_settings )
263  settings->LoadFromFile();
264 
265  wxLogTrace( traceSettings, "Loaded <%s> with schema %d", GetFullFilename(), m_schemaVersion );
266 
267  // If we migrated, clean up the legacy file (with no extension)
268  if( legacy_migrated || migrated )
269  {
270  if( legacy_migrated && m_deleteLegacyAfterMigration && !wxRemoveFile( path.GetFullPath() ) )
271  {
272  wxLogTrace( traceSettings, "Warning: could not remove legacy file %s",
273  path.GetFullPath() );
274  }
275 
276  // And write-out immediately so that we don't lose data if the program later crashes.
277  SaveToFile( aDirectory );
278  }
279 
280  return success;
281 }
282 
283 
285 {
286  bool modified = false;
287 
288  for( auto param : m_params )
289  {
290  modified |= !param->MatchesFile( this );
291  param->Store( this );
292  }
293 
294  return modified;
295 }
296 
297 
299 {
300  for( auto param : m_params )
301  param->SetDefault();
302 }
303 
304 
305 bool JSON_SETTINGS::SaveToFile( const wxString& aDirectory, bool aForce )
306 {
307  if( !m_writeFile )
308  return false;
309 
310  // Default PROJECT won't have a filename set
311  if( m_filename.IsEmpty() )
312  return false;
313 
314  wxFileName path;
315 
316  if( aDirectory.empty() )
317  {
318  path.Assign( m_filename );
319  path.SetExt( getFileExt() );
320  }
321  else
322  {
323  wxString dir( aDirectory );
324  path.Assign( dir, m_filename, getFileExt() );
325  }
326 
327  if( !m_createIfMissing && !path.FileExists() )
328  {
329  wxLogTrace( traceSettings,
330  "File for %s doesn't exist and m_createIfMissing == false; not saving",
331  GetFullFilename() );
332  return false;
333  }
334 
335  // Ensure the path exists, and create it if not.
336  if( !path.DirExists() && !path.Mkdir() )
337  {
338  wxLogTrace( traceSettings, "Warning: could not create path %s, can't save %s",
339  path.GetPath(), GetFullFilename() );
340  return false;
341  }
342 
343  if( ( path.FileExists() && !path.IsFileWritable() ) ||
344  ( !path.FileExists() && !path.IsDirWritable() ) )
345  {
346  wxLogTrace( traceSettings, "File for %s is read-only; not saving", GetFullFilename() );
347  return false;
348  }
349 
350  bool modified = false;
351 
352  for( auto settings : m_nested_settings )
353  modified |= settings->SaveToFile();
354 
355  modified |= Store();
356 
357  if( !modified && !aForce && path.FileExists() )
358  {
359  wxLogTrace( traceSettings, "%s contents not modified, skipping save", GetFullFilename() );
360  return false;
361  }
362  else if( !modified && !aForce && !m_createIfDefault )
363  {
364  wxLogTrace( traceSettings,
365  "%s contents still default and m_createIfDefault == false; not saving",
366  GetFullFilename() );
367  return false;
368  }
369 
370  wxLogTrace( traceSettings, "Saving %s", GetFullFilename() );
371 
373  bool success = true;
374 
375  try
376  {
377  std::stringstream buffer;
378  buffer << std::setw( 2 ) << *this << std::endl;
379 
380  wxFile file( path.GetFullPath(), wxFile::write );
381 
382  if( !file.IsOpened() || !file.Write( buffer.str().c_str(), buffer.str().size() ) )
383  {
384  wxLogTrace( traceSettings, "Warning: could not save %s", GetFullFilename() );
385  success = false;
386  }
387  }
388  catch( nlohmann::json::exception& error )
389  {
390  wxLogTrace( traceSettings, "Catch error: could not save %s. Json error %s",
391  GetFullFilename(), error.what() );
392  success = false;
393  }
394  catch( ... )
395  {
396  wxLogTrace( traceSettings, "Error: could not save %s." );
397  success = false;
398  }
399 
400  return success;
401 }
402 
403 
404 OPT<nlohmann::json> JSON_SETTINGS::GetJson( const std::string& aPath ) const
405 {
406  nlohmann::json::json_pointer ptr = PointerFromString( aPath );
407 
408  if( this->contains( ptr ) )
409  {
410  try
411  {
412  return OPT<nlohmann::json>{ this->at( ptr ) };
413  }
414  catch( ... )
415  {
416  }
417  }
418 
419  return OPT<nlohmann::json>{};
420 }
421 
422 
424 {
425  wxLogTrace( traceSettings, "Migrate() not implemented for %s", typeid( *this ).name() );
426  return false;
427 }
428 
429 
430 bool JSON_SETTINGS::MigrateFromLegacy( wxConfigBase* aLegacyConfig )
431 {
432  wxLogTrace( traceSettings,
433  "MigrateFromLegacy() not implemented for %s", typeid( *this ).name() );
434  return false;
435 }
436 
437 
438 nlohmann::json::json_pointer JSON_SETTINGS::PointerFromString( std::string aPath )
439 {
440  std::replace( aPath.begin(), aPath.end(), '.', '/' );
441  aPath.insert( 0, "/" );
442 
443  nlohmann::json::json_pointer p;
444 
445  try
446  {
447  p = nlohmann::json::json_pointer( aPath );
448  }
449  catch( ... )
450  {
451  wxASSERT_MSG( false, wxT( "Invalid pointer path in PointerFromString!" ) );
452  }
453 
454  return p;
455 }
456 
457 
458 bool JSON_SETTINGS::SetIfPresent( const nlohmann::json& aObj, const std::string& aPath,
459  wxString& aTarget )
460 {
461  nlohmann::json::json_pointer ptr = PointerFromString( aPath );
462 
463  if( aObj.contains( ptr ) && aObj.at( ptr ).is_string() )
464  {
465  aTarget = aObj.at( ptr ).get<wxString>();
466  return true;
467  }
468 
469  return false;
470 }
471 
472 
473 bool JSON_SETTINGS::SetIfPresent( const nlohmann::json& aObj, const std::string& aPath,
474  bool& aTarget )
475 {
476  nlohmann::json::json_pointer ptr = PointerFromString( aPath );
477 
478  if( aObj.contains( ptr ) && aObj.at( ptr ).is_boolean() )
479  {
480  aTarget = aObj.at( ptr ).get<bool>();
481  return true;
482  }
483 
484  return false;
485 }
486 
487 
488 bool JSON_SETTINGS::SetIfPresent( const nlohmann::json& aObj, const std::string& aPath,
489  int& aTarget )
490 {
491  nlohmann::json::json_pointer ptr = PointerFromString( aPath );
492 
493  if( aObj.contains( ptr ) && aObj.at( ptr ).is_number_integer() )
494  {
495  aTarget = aObj.at( ptr ).get<int>();
496  return true;
497  }
498 
499  return false;
500 }
501 
502 
503 bool JSON_SETTINGS::SetIfPresent( const nlohmann::json& aObj, const std::string& aPath,
504  unsigned int& aTarget )
505 {
506  nlohmann::json::json_pointer ptr = PointerFromString( aPath );
507 
508  if( aObj.contains( ptr ) && aObj.at( ptr ).is_number_unsigned() )
509  {
510  aTarget = aObj.at( ptr ).get<unsigned int>();
511  return true;
512  }
513 
514  return false;
515 }
516 
517 
518 template<typename ValueType>
519 bool JSON_SETTINGS::fromLegacy( wxConfigBase* aConfig, const std::string& aKey,
520  const std::string& aDest )
521 {
522  ValueType val;
523 
524  if( aConfig->Read( aKey, &val ) )
525  {
526  try
527  {
528  ( *this )[PointerFromString( aDest )] = val;
529  }
530  catch( ... )
531  {
532  wxASSERT_MSG( false, wxT( "Could not write value in fromLegacy!" ) );
533  return false;
534  }
535 
536  return true;
537  }
538 
539  return false;
540 }
541 
542 
543 // Explicitly declare these because we only support a few types anyway, and it means we can keep
544 // wxConfig detail out of the header file
545 template bool JSON_SETTINGS::fromLegacy<int>( wxConfigBase*, const std::string&,
546  const std::string& );
547 
548 template bool JSON_SETTINGS::fromLegacy<double>( wxConfigBase*, const std::string&,
549  const std::string& );
550 
551 template bool JSON_SETTINGS::fromLegacy<bool>( wxConfigBase*, const std::string&,
552  const std::string& );
553 
554 
555 bool JSON_SETTINGS::fromLegacyString( wxConfigBase* aConfig, const std::string& aKey,
556  const std::string& aDest )
557 {
558  wxString str;
559 
560  if( aConfig->Read( aKey, &str ) )
561  {
562  try
563  {
564  ( *this )[PointerFromString( aDest )] = str.ToUTF8();
565  }
566  catch( ... )
567  {
568  wxASSERT_MSG( false, wxT( "Could not write value in fromLegacyString!" ) );
569  return false;
570  }
571 
572  return true;
573  }
574 
575  return false;
576 }
577 
578 
579 bool JSON_SETTINGS::fromLegacyColor( wxConfigBase* aConfig, const std::string& aKey,
580  const std::string& aDest )
581 {
582  wxString str;
583 
584  if( aConfig->Read( aKey, &str ) )
585  {
587  color.SetFromWxString( str );
588 
589  try
590  {
591  nlohmann::json js = nlohmann::json::array( { color.r, color.g, color.b, color.a } );
592  ( *this )[PointerFromString( aDest )] = js;
593  }
594  catch( ... )
595  {
596  wxASSERT_MSG( false, wxT( "Could not write value in fromLegacyColor!" ) );
597  return false;
598  }
599 
600  return true;
601  }
602 
603  return false;
604 }
605 
606 
608 {
609  wxLogTrace( traceSettings, "AddNestedSettings %s", aSettings->GetFilename() );
610  m_nested_settings.push_back( aSettings );
611 }
612 
613 
615 {
616  if( !aSettings )
617  return;
618 
619  auto it = std::find_if( m_nested_settings.begin(), m_nested_settings.end(),
620  [&aSettings]( const JSON_SETTINGS* aPtr ) {
621  return aPtr == aSettings;
622  } );
623 
624  if( it != m_nested_settings.end() )
625  {
626  wxLogTrace( traceSettings, "Flush and release %s", ( *it )->GetFilename() );
627  ( *it )->SaveToFile();
628  m_nested_settings.erase( it );
629  }
630 
631  aSettings->SetParent( nullptr );
632 }
633 
634 
635 // Specializations to allow conversion between wxString and std::string via JSON_SETTINGS API
636 
637 template<> OPT<wxString> JSON_SETTINGS::Get( const std::string& aPath ) const
638 {
639  if( OPT<nlohmann::json> opt_json = GetJson( aPath ) )
640  return wxString( opt_json->get<std::string>().c_str(), wxConvUTF8 );
641 
642  return NULLOPT;
643 }
644 
645 
646 template<> void JSON_SETTINGS::Set<wxString>( const std::string& aPath, wxString aVal )
647 {
648  ( *this )[PointerFromString( std::move( aPath ) ) ] = aVal.ToUTF8();
649 }
650 
651 // Specializations to allow directly reading/writing wxStrings from JSON
652 
653 void to_json( nlohmann::json& aJson, const wxString& aString )
654 {
655  aJson = aString.ToUTF8();
656 }
657 
658 
659 void from_json( const nlohmann::json& aJson, wxString& aString )
660 {
661  aString = wxString( aJson.get<std::string>().c_str(), wxConvUTF8 );
662 }
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::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
bool m_createIfMissing
Whether or not the backing store file should be created it if doesn't exist.
wxString GetFilename() const
Definition: json_settings.h:64
int color
Definition: DXF_plotter.cpp:61
virtual wxString getLegacyFileExt() const
bool parse(std::istream &aStream, bool aVerbose)
Parse a PCB or footprint file from the given input stream.
SETTINGS_LOC
Definition: json_settings.h:44
virtual bool LoadFromFile(const wxString &aDirectory="")
Loads the backing file from disk and then calls Load()
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.
wxString m_filename
The filename (not including path) of this settings file (inicode)
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.
virtual bool SaveToFile(const wxString &aDirectory="", bool aForce=false)
wxString m_legacy_filename
The filename of the wxConfig legacy file (if different from m_filename)
const auto NULLOPT
Definition: optional.h:9
void to_json(nlohmann::json &aJson, const wxString &aString)
static bool SetIfPresent(const nlohmann::json &aObj, const std::string &aPath, wxString &aTarget)
Sets the given string if the given key/path is present.
std::vector< NESTED_SETTINGS * > m_nested_settings
Nested settings files that live inside this one, if any.
JSON_SETTINGS(const wxString &aFilename, SETTINGS_LOC aLocation, int aSchemaVersion)
Definition: json_settings.h:56
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.
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.
virtual ~JSON_SETTINGS()
const wxChar *const traceSettings
Flag to enable debug output of settings operations and management.
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.
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)