What is the steps for changing an attribute value of an xml file?

Hi,
I created a simple preset management system to save and load my presets as xml files. But I want to change the one of the attribute’s value. I tried to do something like this:

  1. create a memory block
  2. create a file array and fill it with preset files.
  3. load the current file’s content to memory block as data.
  4. (This is the part that I need help) Parse the data, find a parameter that you want to change, then change it
  5. replace the content of the file with new memory block data.
  6. go to beginning and repeat every for all presets

I also tried to create a memoryInputStream to get the memory block data as binary data then I tried to turn it into a string to use string methods to find the part that I want to change. Finally after changing the string , I tried to go through all these steps back and replace the file’s content with new data by using a memoryOutputStream. But it throws errors and it does not work correctly . That is why I didn’t put it here. Could you please help me to find a proper way to do this?

Should I try to create an xml file and copy the content of the file to it, and then try to use xml methods ? Is it possible to show me a few lines about it?
or Should I try to change that parameter value just after the scanning the presets and try to save the file without dealing with parsing? (I don’t want to do this actually because I want to learn how to deal with files.)
or Am I completely on the wrong way ?

Look up the XmlDocument and XmlElement classes. There is an XmlDocument constructor that takes a File& as its parameter, which you can use to examine and modify the contents with, then save the File after making changes to the XML.

Thank you.
I checked that class. This is what i am trying to do:

 Array<File> presetList; // a file array to load all xml files into it.

    // this will go to Users/Shared/MyPlugin/Presets and keep this file path
File file = File::getSpecialLocation(File::commonDocumentsDirectory).getChildFile("MyPlugin/Presets/.a").getParentDirectory().getFullPathName();


 
file.findChildFiles(presetList, File::findFiles, false, "*.Preset"); //fill presetList array with xml files

  for(int i=0; i<presetList.size();i++)
{
    XmlDocument xmlDocument(presetList[i]);    //for every single xml file, create a xml document/
    
    //Find the element that includes "presetindex" tagname

    auto xmlElement = xmlDocument.getDocumentElementIfTagMatches("presetindex");   //i don't know what this stores. Is it in MyPluginPreset element ? 



// after finding the element that has  "presetindex" tagname,  I want to find the attribute that has the same name and  i want to change its value but I couldn't do this part. 

    
    
}

Here is my xml file

 VC2!√<?xml version="1.0" encoding="UTF-8"?> 
 <MyPluginPreset>
       <PARAM id="bpm" value="0.0"/>
       <PARAM id="presetindex" value="6.0"/>
</MyPluginPreset>

I want to locate the presetindex and change its value.Let’s say with 10.0. So after the changing its value it should look like this

 VC2!√<?xml version="1.0" encoding="UTF-8"?> 
 <MyPluginPreset>
       <PARAM id="bpm" value="0.0"/>
       <PARAM id="presetindex" value="10.0"/>
</MyPluginPreset>

I am not good at parsing xml files. I haven’t done it before(even with other languages)This is my first attemp. Could you please guide me to add the remaining part ?

Also these xml files have “VC2” prefix . Without this, when i try to load or save them by setStateInformation and getStateInformation it does not work. Looks like this prefix is needed . I tried to debug it to if it stores the information of the files.

   DBG("File to string: "<< presetList[i].loadFileAsString());

this prints the prefix of files, so it loads the files to presetlist but somehow “loadFileAsString()” is not able to read the whole file content. It only prints “VC2!?, VC2!. or VC2!’”. When try to do the same thing for xmlElement by

  DBG("xml to string: "<< xmlElement->toString());

it throws an error that says the content is empty.

It looks like those bytes before the actual XML are the header used in the chunk data. Those are not part of the XML itself. Not sure how you created those files or why you’re using them as if they are raw XML, but in any case, the “presetIndex” is not an Element, it’s an Attribute. The Element that contains it has the Tag “PARAM”. There are any number of those, all contained inside another Element whose Tag is “MyPluginPreset”. The “MyPluginPreset” Element is your Root Element. The Document itself includes the XML header (that part in the <> brackets just before your Root Element, but not that chunk data tag at the start.

In any case, if you want to work with the XML, then you need to skip over those somehow. For example, your could stream the file into a String, then skip over those first bytes, and pass the resulting String to XmlDocument::parse().

That would get you the XmlDocument, from which you can iterate through all the child XmlElements with the Tag “PARAM”, and get the “id” and “value” Attributes for each. You’d want the String values for the “id” Attributes, and the float values for the “value” Attributes.

Once you have those, you can manipulate them, if desired. Not sure what you plan is after that, but you could then get the string from the XmlDocument, open a stream for writing, write out the chunk header, and then write out the rest of the String you got from the XML.

The XmlElement class has all the powerful XML manipulation tools. You don’t even need XmlDocument in this case. I think you’d want the loop that iterates the files to look like this (haven’t tested this):

for (auto presetFile : presetList)
{
    if (auto xml = parseXML (presetFile))  // parseXML returns std::unique_ptr<XmlElement>
    {
        if (auto indexChild = xml->getChildByAttribute ("id", "presetIndex"))
            indexChild->setAttribute  ("value", "10");

        // add code here to save file or do something else with the modified preset data?
    }
    else
    {
        DBG ("bad XML, could not parse");
    }
}

Although then you have code that is changing all of your presetIndex values to 10, which is probably not what you want.

I’d also echo @HowardAntares comments to get clear about the difference between XML Elements vs Attributes.

Thank you very much.Looks like I need to learn more about xml files.

Thank you for your example code. In my case it always gets into “else”.

I tried to check the place that I am saving and loading the presets but everything is being handled by setStateInformation and getStateInformation.

For saving the presets,

  1. I create a memory block
  2. I get the current data from getStateInformation
    3)I use the replaceWithData to write the memory block data to file.

For loading the presets

  1. create a memory block
  2. load the corresponding file content as data by loadFileAsData
  3. load it to setStateInformation.

So generally instead of parsing I get whole content of the file and save it or load it. It was not that much hard :slight_smile: but now, parsing is being a headache for me

Edit: toString() or loadDataAsString or these kind of functions does not turn the whole file content to string. When try to print the (this is a file object)file.toString() and then try to print it, it only prints the first “header” which is “VC2! and some dots and commas”. So looks like there is problem with this xml structure :confused: I don’t know…
Could you please let me know if you have seen an example on git or somewhere else. It is really hard to find any examples in juce:) especially in my country I might be the only person who uses juce :smiley: Thank you for your code and help.

My suggestion was to stream the file into a String. That way, you can skip over these first bytes of the data, and treat only the rest of the String as XML that should parse properly. You can’t expect the “chunk” header to parse correctly.

1 Like

I was trying to figure out how your preset files were generated in the first place, and where that chunk header (VC2!√) came from. You don’t want that chunk header in there, it’s messing up the XML parse.

If this how you’re generating the preset files, it sounds like the wrong way to do it:

getStateInformation is meant to be called by the host, not by you. If your code looks like the code in the APVTS tutorial, then it’s calling copyXmlToBinary which you don’t want when writing to a plain XML file.

Instead, your preset manager can just do this to write the APVTS state to a file:

auto state = apvts.copyState();
std::unique_ptr<XmlElement> xml (state.createXml());
xml->writeTo (presetFileDestination);
1 Like

Actually i tried:) I tried but the string conversion methods do not get the data after the “<” or “>” . For example i tried to find the most wide ranged method to read the inputstream. But it reads only the beginning of the file. Again my files look like this

 VC2!à<?xml version="1.0" encoding="UTF-8"?> 
   <MyPluginPreset>
   <PARAM id="bpm" value="0.0"/>
   <PARAM id="presetindex" value="6.0"/>
   </MyPluginPreset>

(Btw this is what i see when try to open these files with texteditor. )
(It always adds that strange header to it. )

And this is my code to read it as string

   Array<File> presetList; // a file array to load all xml files into it.

// this will go to Users/Shared/MyPlugin/Presets and keep this file path
 File file =File::getSpecialLocation(File::commonDocumentsDirectory).getChildFile("MyPlugin/Presets/.a").getParentDirectory().getFullPathName();
 file.findChildFiles(presetList, File::findFiles, false, "*.Preset"); //fill presetList array with xml files

  for(int i=0; i<presetList.size();i++)
{
           MemoryBlock memoryBlock = MemoryBlock();
           presetList[i].loadFileAsData (memoryBlock);
         
           std::unique_ptr<MemoryInputStream> memoryInputStream = std::make_unique<MemoryInputStream> (memoryBlock, false);
    
           DBG("memoryInputStream->readEntireStreamAsString()"<<memoryInputStream->readEntireStreamAsString());
}

this is what i got in output window.

I can’t even get it as a string :confused:

Here in these files, juce provides a save as and load feature by using askUserToSaveState() and askUserToLoadState
I tried to mimic this technic.

Yes, this is exactly how my getStateInformation and setStateInformation look.

I have a different class named saveAndLoad to handle preset managment and it is derived from juce_audio_plugin_client module-standalone example as i mentioned above

OK, well I gave you the code that works to write the APVTS state as a plain XML file in my last post, so I’m not sure if you’re arguing against that at this point.

Right. And askUserToSaveState calls processor->getStateInformation, so that’s still the source if its data. And, as I mentioned above, the getStateInformation implementation as shown in the APVTS tutorial includes a call to copyXmlToBinary. Take a look at that method, and you’ll see that it’s adding a magicXmlNumber to the start of the memory block, which is (as far as I can tell) the source of that 4-char string you’re seeing before your XML data. You don’t want that when writing to a plain XML file. The code I posted above avoids all that.

If you really had to go with using that same getStateInformation implementation as your data source, you could then remove the “magic XML number” using the JUCE method getXmlFromBinary… but that’s a bunch of extra effort for no purpose.

1 Like

Yes you did :slight_smile: Thank you very much for this :pray:

I checked it. You’re right. But my preset manager is also connected to so many other places :smiley: i attempted to create a new one the way you mentioned. It works. I am able to parse it easily. But somehow my load, save as and save current function stoped working properly :confused: So for now i had to delete the header, parse it, then add the magic number and then override it. Thanks to you, right now, i am able to do all of them. This way i can still use setStateinformation and getStateInformation to synchronize everything. Since i still use setStateinformation and getStateInformation, i know that it is not the correct way to do it. But for now i need to keep it as it is till i create a new preset manager. I have some other parts to change.

@leigh @HowardAntares
I greatly appreciate your help, Thank you very much.:pray:

Here is my code

   // PARSE_XML_FILE

  Array<File> presetList; // a file array to load all xml files into it.

// this will go to Users/Shared/MyPlugin/Presets and keep this file path
 File file =File::getSpecialLocation(File::commonDocumentsDirectory).getChildFile("MyPlugin/Presets/.a").getParentDirectory().getFullPathName();
file.findChildFiles(presetList, File::findFiles, false, "*.Preset"); //fill presetList array with xml files


for(int i=0; i<presetList.size();i++)
{
   

           MemoryBlock memoryBlock = MemoryBlock();// create a memory block
           presetList[i].loadFileAsData (memoryBlock);// load the preset to memory block
           // delete the magicXmlNumber
           auto xmlElement= LunarLanderAudioProcessor::getXmlFromBinary(memoryBlock.getData(),
                                                                        (int)memoryBlock.getSize());
            
            
         //  DBG("xmlElement->toString()"<<xmlElement->toString());
    
        // Start Parsing the xml. Find the the element with tagname
        if (xmlElement->hasTagName ("MyPluginPreset"))
        {
            // now we'll iterate its sub-elements looking for 'PARAM' elements..
            forEachXmlChildElement (*xmlElement, e)
            {
                if (e->hasTagName ("PARAM")) // find the "PARAM" sub-element
                {
                   
    
                    // in PARAM look for 'id' arrtibute. If the 'id' attribute equals to "presetmenustorer" then get the value of the 'value' attribute
                    String attributeValueAsString   = e->getStringAttribute("id");
                    if(attributeValueAsString.equalsIgnoreCase("presetmenustorer"))
                       
                    {
                        String secondAttributeName   = e->getStringAttribute("value");
                        DBG("attributeValueAsString :"<<attributeValueAsString);
                        DBG("secondAttributeName :"<<secondAttributeName);
                        //once you find the 'value' attribute, change its value according to alphabetical order index => (i)
                        e->setAttribute("value",float(i));
                    }
                }
            }
        }
    
    
    
    //==============================================================================
    // magic number to identify memory blocks that we've stored as XML
    // took this from void AudioProcessor::copyXmlToBinary (const XmlElement& xml, juce::MemoryBlock& destData)
    const uint32 magicXmlNumber = 0x21324356;
          
                     MemoryBlock destData;// create a memory block
                     MemoryOutputStream out (destData, false);// create a outputstream to load the xml content to memory block
                     out.writeInt (magicXmlNumber);// Add the magicnumber to xml
                     out.writeInt (0);
                     xmlElement->writeTo (out, XmlElement::TextFormat().singleLine());
                     out.writeByte (0);
               
                    // go back and write the string length..
                    static_cast<uint32*> (destData.getData())[1] = ByteOrder::swapIfBigEndian ((uint32) destData.getSize() -9);
                 
                 //DBG("destData.toString()"<<destData.toString());
              
                 juce::String filePath = presetList[mPresetMenuIndexStorer.getValue()].getFileName();
                 presetList[i].replaceWithData(destData.getData(), destData.getSize());

        }

This works. But the only issue is that , it goes to beginning of the file and rewrite it to the end of xml .So my xml file looks like this

   VC2!à<?xml version="1.0" encoding="UTF-8"?> 
  <MyPluginPreset>
  <PARAM id="bpm" value="0.0"/>
  <PARAM id="presetmenustorer" value="6.0"/>
  </MyPluginPreset> ginPreset>ginPreset>ginPreset>ginPreset>

I think it is about this

 // go back and write the string length..
    static_cast<uint32*> (destData.getData())[1] = ByteOrder::swapIfBigEndian ((uint32) destData.getSize() -9);

But i haven’t found the proper way to fix it. Any suggestions ? :pray: