Skip to content

XML and JSON ​

Reads XML and JSON files and shows the parsed result.

  • XmlDocument and XmlElement
  • JSON::parse and var
  • Displaying parsed trees

Source: examples/Utilities/XMLandJSONDemo.h on GitHub.

cpp
/*******************************************************************************
 The block below describes the properties of this PIP. A PIP is a short snippet
 of code that can be read by the Projucer and used to generate a JUCE project.

 BEGIN_JUCE_PIP_METADATA

 name:             XMLandJSONDemo
 version:          1.0.0
 vendor:           JUCE
 website:          http://juce.com
 description:      Reads XML and JSON files.

 dependencies:     juce_core, juce_data_structures, juce_events, juce_graphics,
                   juce_gui_basics, juce_gui_extra
 exporters:        xcode_mac, vs2022, vs2026, linux_make, androidstudio,
                   xcode_iphone

 moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1

 type:             Component
 mainClass:        XMLandJSONDemo

 useLocalCopy:     1

 END_JUCE_PIP_METADATA

*******************************************************************************/

#pragma once

#include "../Assets/DemoUtilities.h"

//==============================================================================
class XmlTreeItem final : public TreeViewItem
{
public:
    XmlTreeItem (XmlElement& x)  : xml (x)    {}

    String getUniqueName() const override
    {
        if (xml.getTagName().isEmpty())
            return "unknown";

        return xml.getTagName();
    }

    bool mightContainSubItems() override
    {
        return xml.getFirstChildElement() != nullptr;
    }

    void paintItem (Graphics& g, int width, int height) override
    {
        // if this item is selected, fill it with a background colour..
        if (isSelected())
            g.fillAll (Colours::blue.withAlpha (0.3f));

        // use a "colour" attribute in the xml tag for this node to set the text colour..
        g.setColour (Colour::fromString (xml.getStringAttribute ("colour", "ff000000")));
        g.setFont ((float) height * 0.7f);

        // draw the xml element's tag name..
        g.drawText (xml.getTagName(),
                    4, 0, width - 4, height,
                    Justification::centredLeft, true);
    }

    void itemOpennessChanged (bool isNowOpen) override
    {
        if (isNowOpen)
        {
            // if we've not already done so, we'll now add the tree's sub-items. You could
            // also choose to delete the existing ones and refresh them if that's more suitable
            // in your app.
            if (getNumSubItems() == 0)
            {
                // create and add sub-items to this node of the tree, corresponding to
                // each sub-element in the XML..

                for (auto* child : xml.getChildIterator())
                    if (child != nullptr)
                        addSubItem (new XmlTreeItem (*child));
            }
        }
        else
        {
            // in this case, we'll leave any sub-items in the tree when the node gets closed,
            // though you could choose to delete them if that's more appropriate for
            // your application.
        }
    }

private:
    XmlElement& xml;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (XmlTreeItem)
};

//==============================================================================
class JsonTreeItem final : public TreeViewItem
{
public:
    JsonTreeItem (Identifier i, var value)
        : identifier (i),
          json (value)
    {}

    String getUniqueName() const override
    {
        return identifier.toString() + "_id";
    }

    bool mightContainSubItems() override
    {
        if (auto* obj = json.getDynamicObject())
            return obj->getProperties().size() > 0;

        return json.isArray();
    }

    void paintItem (Graphics& g, int width, int height) override
    {
        // if this item is selected, fill it with a background colour..
        if (isSelected())
            g.fillAll (Colours::blue.withAlpha (0.3f));

        g.setColour (Colours::black);
        g.setFont ((float) height * 0.7f);

        // draw the element's tag name..
        g.drawText (getText(),
                    4, 0, width - 4, height,
                    Justification::centredLeft, true);
    }

    void itemOpennessChanged (bool isNowOpen) override
    {
        if (isNowOpen)
        {
            // if we've not already done so, we'll now add the tree's sub-items. You could
            // also choose to delete the existing ones and refresh them if that's more suitable
            // in your app.
            if (getNumSubItems() == 0)
            {
                // create and add sub-items to this node of the tree, corresponding to
                // the type of object this var represents

                if (json.isArray())
                {
                    for (int i = 0; i < json.size(); ++i)
                    {
                        auto& child = json[i];
                        jassert (! child.isVoid());
                        addSubItem (new JsonTreeItem ({}, child));
                    }
                }
                else if (auto* obj = json.getDynamicObject())
                {
                    auto& props = obj->getProperties();

                    for (int i = 0; i < props.size(); ++i)
                    {
                        auto id = props.getName (i);

                        auto child = props[id];
                        jassert (! child.isVoid());

                        addSubItem (new JsonTreeItem (id, child));
                    }
                }
            }
        }
        else
        {
            // in this case, we'll leave any sub-items in the tree when the node gets closed,
            // though you could choose to delete them if that's more appropriate for
            // your application.
        }
    }

private:
    Identifier identifier;
    var json;

    /** Returns the text to display in the tree.
        This is a little more complex for JSON than XML as nodes can be strings, objects or arrays.
     */
    String getText() const
    {
        String text;

        if (identifier.isValid())
            text << identifier.toString();

        if (! json.isVoid())
        {
            if (text.isNotEmpty() && (! json.isArray()))
                text << ": ";

            if (json.isObject() && (! identifier.isValid()))
                text << "[Array]";
            else if (! json.isArray())
                text << json.toString();
        }

        return text;
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JsonTreeItem)
};

//==============================================================================
class XMLandJSONDemo final : public Component,
                             private CodeDocument::Listener
{
public:
    /** The type of database to parse. */
    enum Type
    {
        xml,
        json
    };

    XMLandJSONDemo()
    {
        setOpaque (true);

        addAndMakeVisible (typeBox);
        typeBox.addItem ("XML",  1);
        typeBox.addItem ("JSON", 2);

        typeBox.onChange = [this]
        {
            if (typeBox.getSelectedId() == 1)
                reset (xml);
            else
                reset (json);
        };

        comboBoxLabel.attachToComponent (&typeBox, true);

        addAndMakeVisible (codeDocumentComponent);
        codeDocument.addListener (this);

        resultsTree.setTitle ("Results");
        addAndMakeVisible (resultsTree);
        resultsTree.setColour (TreeView::backgroundColourId, Colours::white);
        resultsTree.setDefaultOpenness (true);

        addAndMakeVisible (errorMessage);
        errorMessage.setReadOnly (true);
        errorMessage.setMultiLine (true);
        errorMessage.setCaretVisible (false);
        errorMessage.setColour (TextEditor::outlineColourId, Colours::transparentWhite);
        errorMessage.setColour (TextEditor::shadowColourId,  Colours::transparentWhite);

        typeBox.setSelectedId (1);

        setSize (500, 500);
    }

    ~XMLandJSONDemo() override
    {
        resultsTree.setRootItem (nullptr);
    }

    void paint (Graphics& g) override
    {
        g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground));
    }

    void resized() override
    {
        auto area = getLocalBounds();

        typeBox.setBounds (area.removeFromTop (36).removeFromRight (150).reduced (8));
        codeDocumentComponent.setBounds (area.removeFromTop (area.getHeight() / 2).reduced (8));
        resultsTree          .setBounds (area.reduced (8));
        errorMessage         .setBounds (resultsTree.getBounds());
    }

private:
    ComboBox typeBox;
    Label comboBoxLabel { {}, "Database Type:" };
    CodeDocument codeDocument;
    CodeEditorComponent codeDocumentComponent  { codeDocument, nullptr };
    TreeView resultsTree;

    std::unique_ptr<TreeViewItem> rootItem;
    std::unique_ptr<XmlElement> parsedXml;
    TextEditor errorMessage;

    void rebuildTree()
    {
        std::unique_ptr<XmlElement> openness;

        if (rootItem != nullptr)
            openness = rootItem->getOpennessState();

        createNewRootNode();

        if (openness != nullptr && rootItem != nullptr)
            rootItem->restoreOpennessState (*openness);
    }

    void createNewRootNode()
    {
        // clear the current tree
        resultsTree.setRootItem (nullptr);
        rootItem.reset();

        // try and parse the editor's contents
        switch (typeBox.getSelectedItemIndex())
        {
            case xml:           rootItem.reset (rebuildXml());        break;
            case json:          rootItem.reset (rebuildJson());       break;
            default:            rootItem.reset();                     break;
        }

        // if we have a valid TreeViewItem hide any old error messages and set our TreeView to use it
        if (rootItem != nullptr)
            errorMessage.clear();

        errorMessage.setVisible (! errorMessage.isEmpty());

        resultsTree.setRootItem (rootItem.get());
    }

    /** Parses the editor's contents as XML. */
    TreeViewItem* rebuildXml()
    {
        parsedXml.reset();

        XmlDocument doc (codeDocument.getAllContent());
        parsedXml = doc.getDocumentElement();

        if (parsedXml.get() == nullptr)
        {
            auto error = doc.getLastParseError();

            if (error.isEmpty())
                error = "Unknown error";

            errorMessage.setText ("Error parsing XML: " + error, dontSendNotification);

            return nullptr;
        }

        return new XmlTreeItem (*parsedXml);
    }

    /** Parses the editor's contents as JSON. */
    TreeViewItem* rebuildJson()
    {
        var parsedJson;
        auto result = JSON::parse (codeDocument.getAllContent(), parsedJson);

        if (! result.wasOk())
        {
            errorMessage.setText ("Error parsing JSON: " + result.getErrorMessage());
            return nullptr;
        }

        return new JsonTreeItem (Identifier(), parsedJson);
    }

    /** Clears the editor and loads some default text. */
    void reset (Type type)
    {
        switch (type)
        {
            case xml:   codeDocument.replaceAllContent (loadEntireAssetIntoString ("treedemo.xml")); break;
            case json:  codeDocument.replaceAllContent (loadEntireAssetIntoString ("juce_module_info")); break;
            default:    codeDocument.replaceAllContent ({}); break;
        }
    }

    void codeDocumentTextInserted (const String&, int) override     { rebuildTree(); }
    void codeDocumentTextDeleted (int, int) override                { rebuildTree(); }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (XMLandJSONDemo)
};

Personal study notes on JUCE. Not affiliated with or endorsed by the JUCE team.