The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
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.

362 lines
12KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library - "Jules' Utility Class Extensions"
  4. Copyright 2004-11 by Raw Material Software Ltd.
  5. ------------------------------------------------------------------------------
  6. JUCE can be redistributed and/or modified under the terms of the GNU General
  7. Public License (Version 2), as published by the Free Software Foundation.
  8. A copy of the license is included in the JUCE distribution, or can be found
  9. online at www.gnu.org/licenses.
  10. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
  11. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  12. A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  13. ------------------------------------------------------------------------------
  14. To release a closed-source product which uses JUCE, commercial licenses are
  15. available: visit www.rawmaterialsoftware.com/juce for more information.
  16. ==============================================================================
  17. */
  18. #include "../core/juce_StandardHeader.h"
  19. BEGIN_JUCE_NAMESPACE
  20. #include "juce_PropertiesFile.h"
  21. #include "../io/files/juce_TemporaryFile.h"
  22. #include "../io/files/juce_FileInputStream.h"
  23. #include "../io/files/juce_FileOutputStream.h"
  24. #include "../io/streams/juce_BufferedInputStream.h"
  25. #include "../io/streams/juce_SubregionStream.h"
  26. #include "../io/streams/juce_GZIPDecompressorInputStream.h"
  27. #include "../io/streams/juce_GZIPCompressorOutputStream.h"
  28. #include "../memory/juce_ScopedPointer.h"
  29. #include "../core/juce_SystemStats.h"
  30. #include "../threads/juce_InterProcessLock.h"
  31. #include "../text/juce_XmlDocument.h"
  32. //==============================================================================
  33. namespace PropertyFileConstants
  34. {
  35. static const int magicNumber = (int) ByteOrder::littleEndianInt ("PROP");
  36. static const int magicNumberCompressed = (int) ByteOrder::littleEndianInt ("CPRP");
  37. static const char* const fileTag = "PROPERTIES";
  38. static const char* const valueTag = "VALUE";
  39. static const char* const nameAttribute = "name";
  40. static const char* const valueAttribute = "val";
  41. }
  42. //==============================================================================
  43. PropertiesFile::Options::Options()
  44. : commonToAllUsers (false),
  45. ignoreCaseOfKeyNames (false),
  46. millisecondsBeforeSaving (3000),
  47. storageFormat (PropertiesFile::storeAsXML),
  48. processLock (nullptr)
  49. {
  50. }
  51. File PropertiesFile::Options::getDefaultFile() const
  52. {
  53. // mustn't have illegal characters in this name..
  54. jassert (applicationName == File::createLegalFileName (applicationName));
  55. #if JUCE_MAC || JUCE_IOS
  56. File dir (commonToAllUsers ? "/Library/"
  57. : "~/Library/");
  58. if (osxLibrarySubFolder != "Preferences" && osxLibrarySubFolder != "Application Support")
  59. {
  60. /* The PropertiesFile class always used to put its settings files in "Library/Preferences", but Apple
  61. have changed their advice, and now stipulate that settings should go in "Library/Application Support".
  62. Because older apps would be broken by a silent change in this class's behaviour, you must now
  63. explicitly set the osxLibrarySubFolder value to indicate which path you want to use.
  64. In newer apps, you should always set this to "Application Support".
  65. If your app needs to load settings files that were created by older versions of juce and
  66. you want to maintain backwards-compatibility, then you can set this to "Preferences".
  67. But.. for better Apple-compliance, the recommended approach would be to write some code that
  68. finds your old settings files in ~/Library/Preferences, moves them to ~/Library/Application Support,
  69. and then uses the new path.
  70. */
  71. jassertfalse;
  72. dir = dir.getChildFile ("Application Support");
  73. }
  74. else
  75. {
  76. dir = dir.getChildFile (osxLibrarySubFolder);
  77. }
  78. if (folderName.isNotEmpty())
  79. dir = dir.getChildFile (folderName);
  80. #elif JUCE_LINUX || JUCE_ANDROID
  81. const File dir ((commonToAllUsers ? "/var/" : "~/")
  82. + (folderName.isNotEmpty() ? folderName
  83. : ("." + applicationName)));
  84. #elif JUCE_WINDOWS
  85. File dir (File::getSpecialLocation (commonToAllUsers ? File::commonApplicationDataDirectory
  86. : File::userApplicationDataDirectory));
  87. if (dir == File::nonexistent)
  88. return File::nonexistent;
  89. dir = dir.getChildFile (folderName.isNotEmpty() ? folderName
  90. : applicationName);
  91. #endif
  92. return dir.getChildFile (applicationName)
  93. .withFileExtension (filenameSuffix);
  94. }
  95. //==============================================================================
  96. PropertiesFile::PropertiesFile (const File& file_, const Options& options_)
  97. : PropertySet (options.ignoreCaseOfKeyNames),
  98. file (file_), options (options_),
  99. loadedOk (false), needsWriting (false)
  100. {
  101. initialise();
  102. }
  103. PropertiesFile::PropertiesFile (const Options& options_)
  104. : PropertySet (options.ignoreCaseOfKeyNames),
  105. file (options_.getDefaultFile()), options (options_),
  106. loadedOk (false), needsWriting (false)
  107. {
  108. initialise();
  109. }
  110. void PropertiesFile::initialise()
  111. {
  112. // You need to correctly specify just one storage format for the file
  113. ProcessScopedLock pl (createProcessLock());
  114. if (pl != nullptr && ! pl->isLocked())
  115. return; // locking failure..
  116. ScopedPointer<InputStream> fileStream (file.createInputStream());
  117. if (fileStream != nullptr)
  118. {
  119. int magicNumber = fileStream->readInt();
  120. if (magicNumber == PropertyFileConstants::magicNumberCompressed)
  121. {
  122. fileStream = new GZIPDecompressorInputStream (new SubregionStream (fileStream.release(), 4, -1, true), true);
  123. magicNumber = PropertyFileConstants::magicNumber;
  124. }
  125. if (magicNumber == PropertyFileConstants::magicNumber)
  126. {
  127. loadedOk = true;
  128. BufferedInputStream in (fileStream.release(), 2048, true);
  129. int numValues = in.readInt();
  130. while (--numValues >= 0 && ! in.isExhausted())
  131. {
  132. const String key (in.readString());
  133. const String value (in.readString());
  134. jassert (key.isNotEmpty());
  135. if (key.isNotEmpty())
  136. getAllProperties().set (key, value);
  137. }
  138. }
  139. else
  140. {
  141. // Not a binary props file - let's see if it's XML..
  142. fileStream = nullptr;
  143. XmlDocument parser (file);
  144. ScopedPointer<XmlElement> doc (parser.getDocumentElement (true));
  145. if (doc != nullptr && doc->hasTagName (PropertyFileConstants::fileTag))
  146. {
  147. doc = parser.getDocumentElement();
  148. if (doc != nullptr)
  149. {
  150. loadedOk = true;
  151. forEachXmlChildElementWithTagName (*doc, e, PropertyFileConstants::valueTag)
  152. {
  153. const String name (e->getStringAttribute (PropertyFileConstants::nameAttribute));
  154. if (name.isNotEmpty())
  155. {
  156. getAllProperties().set (name,
  157. e->getFirstChildElement() != nullptr
  158. ? e->getFirstChildElement()->createDocument (String::empty, true)
  159. : e->getStringAttribute (PropertyFileConstants::valueAttribute));
  160. }
  161. }
  162. }
  163. else
  164. {
  165. // must be a pretty broken XML file we're trying to parse here,
  166. // or a sign that this object needs an InterProcessLock,
  167. // or just a failure reading the file. This last reason is why
  168. // we don't jassertfalse here.
  169. }
  170. }
  171. }
  172. }
  173. else
  174. {
  175. loadedOk = ! file.exists();
  176. }
  177. }
  178. PropertiesFile::~PropertiesFile()
  179. {
  180. if (! saveIfNeeded())
  181. jassertfalse;
  182. }
  183. InterProcessLock::ScopedLockType* PropertiesFile::createProcessLock() const
  184. {
  185. return options.processLock != nullptr ? new InterProcessLock::ScopedLockType (*options.processLock) : nullptr;
  186. }
  187. bool PropertiesFile::saveIfNeeded()
  188. {
  189. const ScopedLock sl (getLock());
  190. return (! needsWriting) || save();
  191. }
  192. bool PropertiesFile::needsToBeSaved() const
  193. {
  194. const ScopedLock sl (getLock());
  195. return needsWriting;
  196. }
  197. void PropertiesFile::setNeedsToBeSaved (const bool needsToBeSaved_)
  198. {
  199. const ScopedLock sl (getLock());
  200. needsWriting = needsToBeSaved_;
  201. }
  202. bool PropertiesFile::save()
  203. {
  204. const ScopedLock sl (getLock());
  205. stopTimer();
  206. if (file == File::nonexistent
  207. || file.isDirectory()
  208. || ! file.getParentDirectory().createDirectory())
  209. return false;
  210. if (options.storageFormat == storeAsXML)
  211. {
  212. XmlElement doc (PropertyFileConstants::fileTag);
  213. for (int i = 0; i < getAllProperties().size(); ++i)
  214. {
  215. XmlElement* const e = doc.createNewChildElement (PropertyFileConstants::valueTag);
  216. e->setAttribute (PropertyFileConstants::nameAttribute, getAllProperties().getAllKeys() [i]);
  217. // if the value seems to contain xml, store it as such..
  218. XmlElement* const childElement = XmlDocument::parse (getAllProperties().getAllValues() [i]);
  219. if (childElement != nullptr)
  220. e->addChildElement (childElement);
  221. else
  222. e->setAttribute (PropertyFileConstants::valueAttribute,
  223. getAllProperties().getAllValues() [i]);
  224. }
  225. ProcessScopedLock pl (createProcessLock());
  226. if (pl != nullptr && ! pl->isLocked())
  227. return false; // locking failure..
  228. if (doc.writeToFile (file, String::empty))
  229. {
  230. needsWriting = false;
  231. return true;
  232. }
  233. }
  234. else
  235. {
  236. ProcessScopedLock pl (createProcessLock());
  237. if (pl != nullptr && ! pl->isLocked())
  238. return false; // locking failure..
  239. TemporaryFile tempFile (file);
  240. ScopedPointer <OutputStream> out (tempFile.getFile().createOutputStream());
  241. if (out != nullptr)
  242. {
  243. if (options.storageFormat == storeAsCompressedBinary)
  244. {
  245. out->writeInt (PropertyFileConstants::magicNumberCompressed);
  246. out->flush();
  247. out = new GZIPCompressorOutputStream (out.release(), 9, true);
  248. }
  249. else
  250. {
  251. // have you set up the storage option flags correctly?
  252. jassert (options.storageFormat == storeAsBinary);
  253. out->writeInt (PropertyFileConstants::magicNumber);
  254. }
  255. const int numProperties = getAllProperties().size();
  256. out->writeInt (numProperties);
  257. for (int i = 0; i < numProperties; ++i)
  258. {
  259. out->writeString (getAllProperties().getAllKeys() [i]);
  260. out->writeString (getAllProperties().getAllValues() [i]);
  261. }
  262. out = nullptr;
  263. if (tempFile.overwriteTargetFileWithTemporary())
  264. {
  265. needsWriting = false;
  266. return true;
  267. }
  268. }
  269. }
  270. return false;
  271. }
  272. void PropertiesFile::timerCallback()
  273. {
  274. saveIfNeeded();
  275. }
  276. void PropertiesFile::propertyChanged()
  277. {
  278. sendChangeMessage();
  279. needsWriting = true;
  280. if (options.millisecondsBeforeSaving > 0)
  281. startTimer (options.millisecondsBeforeSaving);
  282. else if (options.millisecondsBeforeSaving == 0)
  283. saveIfNeeded();
  284. }
  285. END_JUCE_NAMESPACE