Build XPath for node from XmlReader - c#

I am writing an application which parses dynamic xml from various sources and traverses the XML and returns all the unique elements.
Given the sometimes very large size of the Xml files I am using a XmlReader to parse the Xml structure due to memory constraints.
public IDictionary<string, int> Discover(string filePath)
{
Dictionary<string, string> nodeTable = new Dictionary<string, string>();
using (XmlReader reader = XmlReader.Create(filePath))
{
while (!reader.EOF)
{
if (reader.NodeType == XmlNodeType.Element)
{
if (!nodeTable.ContainsKey(reader.LocalName))
{
nodeTable.Add(reader.LocalName, reader.Depth);
}
}
reader.Read();
}
}
Debug.WriteLine("The node table has {0} items.", nodeTable.Count);
return nodeTable;
}
This works a treat and is nice and performant, however the final piece of the puzzle eludes me, I am trying to generate the XPath for each element.
Now, this at first seemed straight forward using something like this.
var elements = new Stack<string>();
while (reader.Read())
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
elements.Push(reader.LocalName);
break;
case XmlNodeType.EndElement:
elements.Pop();
break;
case XmlNodeType.Text:
path = string.Join("/", elements.Reverse());
break;
}
}
But this only really gives me one part of the solution. Given that I wish to return the XPath for every node in the tree which contains data and also detect if a given node tree contains nested collections of data.
i.e.
<customers>
<customer id=2>
<name>ted smith</name>
<addresses>
<address1>
<line1></line1>
</address1>
<address2>
<line1></line1>
<line2></line2>
</address2>
</addresses>
</customer>
<customer id=322>
<name>smith mcsmith</name>
<addresses>
<address1>
<line1></line1>
<line2></line2>
</address1>
<address2>
<line1></line1>
<line2></line2>
</address2>
</addresses>
</customer>
</customers>
Keeping in mind the data is completely dynamic and the schema is unknown.
So the output should include
/customer/name
/customer/address1/line1
/customer/address1/line2
/customer/address2/line1
/customer/address2/line2

I like using recursive method rather than push/pop. See code below
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.IO;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
string input =
"<customers>" +
"<customer id=\"2\">" +
"<name>ted smith</name>" +
"<addresses>" +
"<address1>" +
"<line1></line1>" +
"</address1>" +
"<address2>" +
"<line1></line1>" +
"<line2></line2>" +
"</address2>" +
"</addresses>" +
"</customer>" +
"<customer id=\"322\">" +
"<name>smith mcsmith</name>" +
"<addresses>" +
"<address1>" +
"<line1></line1>" +
"<line2></line2>" +
"</address1>" +
"<address2>" +
"<line1></line1>" +
"<line2></line2>" +
"</address2>" +
"</addresses>" +
"</customer>" +
"</customers>";
StringReader sReader = new StringReader(input);
XmlReader reader = XmlReader.Create(sReader);
Node root = new Node();
ReadNode(reader, root);
}
static bool ReadNode(XmlReader reader, Node node)
{
Boolean done = false;
Boolean endElement = false;
while(done = reader.Read())
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
if (node.name.Length == 0)
{
node.name = reader.Name;
GetAttrubutes(reader, node);
}
else
{
Node newNode = new Node();
newNode.name = reader.Name;
if (node.children == null)
{
node.children = new List<Node>();
}
node.children.Add(newNode);
GetAttrubutes(reader, newNode);
done = ReadNode(reader, newNode);
}
break;
case XmlNodeType.EndElement:
endElement = true;
break;
case XmlNodeType.Text:
node.text = reader.Value;
break;
case XmlNodeType.Attribute:
if (node.attributes == null)
{
node.attributes = new Dictionary<string, string>();
}
node.attributes.Add(reader.Name, reader.Value);
break;
}
if (endElement)
break;
}
return done;
}
static void GetAttrubutes(XmlReader reader, Node node)
{
for (int i = 0; i < reader.AttributeCount; i++)
{
if (i == 0) node.attributes = new Dictionary<string, string>();
reader.MoveToNextAttribute();
node.attributes.Add(reader.Name, reader.Value);
}
}
}
public class Node
{
public string name = string.Empty;
public string text = string.Empty;
public Dictionary<string, string> attributes = null;
public List<Node> children = null;
}
}
​

Related

Url Xml Parsing In c#

i want to get a data from a xml site but i want spesific data i want to get USD/TRY, GBP/TRY and EUR/TRY Forex Buying values i dont know how to split those values from the data i have a test console program and the is like this
using System;
using System.Xml;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
string XmlUrl = "https://www.tcmb.gov.tr/kurlar/today.xml";
XmlTextReader reader = new XmlTextReader(XmlUrl);
while (reader.Read())
{
switch (reader.NodeType)
{
case XmlNodeType.Element: // The node is an element.
Console.Write("<" + reader.Name);
while (reader.MoveToNextAttribute()) // Read the attributes.
Console.Write(" " + reader.Name + "='" + reader.Value + "'");
Console.Write(">");
Console.WriteLine(">");
break;
case XmlNodeType.Text: //Display the text in each element.
Console.WriteLine(reader.Value);
break;
case XmlNodeType.EndElement: //Display the end of the element.
Console.Write("</" + reader.Name);
Console.WriteLine(">");
break;
}
}
}
}
}
how can i split the values from i want from the xml
My desired output is this
public class ParaBirimi
{
public decimal ForexBuying { get; set; }
public string Name { get; set; }//Values like USD GBP EUR
}
class to a list
It is better to use LINQ to XML API. It is available in the .Net Framework since 2007.
Here is your starting point. You can extend it to read any attribute or element.
XML fragment
<Tarih_Date Tarih="28.05.2021" Date="05/28/2021" Bulten_No="2021/100">
<Currency CrossOrder="0" Kod="USD" CurrencyCode="USD">
<Unit>1</Unit>
<Isim>ABD DOLARI</Isim>
<CurrencyName>US DOLLAR</CurrencyName>
<ForexBuying>8.5496</ForexBuying>
<ForexSelling>8.5651</ForexSelling>
<BanknoteBuying>8.5437</BanknoteBuying>
<BanknoteSelling>8.5779</BanknoteSelling>
<CrossRateUSD/>
<CrossRateOther/>
</Currency>
<Currency CrossOrder="1" Kod="AUD" CurrencyCode="AUD">
<Unit>1</Unit>
<Isim>AVUSTRALYA DOLARI</Isim>
<CurrencyName>AUSTRALIAN DOLLAR</CurrencyName>
<ForexBuying>6.5843</ForexBuying>
<ForexSelling>6.6272</ForexSelling>
<BanknoteBuying>6.5540</BanknoteBuying>
<BanknoteSelling>6.6670</BanknoteSelling>
<CrossRateUSD>1.2954</CrossRateUSD>
<CrossRateOther/>
</Currency>
...
</Tarih_Date>
c#
void Main()
{
const string URL = #"https://www.tcmb.gov.tr/kurlar/today.xml";
XDocument xdoc = XDocument.Load(URL);
foreach (XElement elem in xdoc.Descendants("Currency"))
{
Console.WriteLine("CrossOrder: '{1}', Kod: '{1}', CurrencyCode: '{2}', Isim: '{3}', CurrencyName: '{4}'{0}"
, Environment.NewLine
, elem.Attribute("CrossOrder").Value
, elem.Attribute("Kod").Value
, elem.Attribute("CurrencyCode").Value
, elem.Element("Isim").Value
, elem.Element("CurrencyName").Value
);
}
}
Output
CrossOrder: '0', Kod: '0', CurrencyCode: 'USD', Isim: 'USD', CurrencyName: 'ABD DOLARI'
CrossOrder: '1', Kod: '1', CurrencyCode: 'AUD', Isim: 'AUD', CurrencyName: 'AVUSTRALYA DOLARI'

using XmlReader to get child nodes without knowing their names(in .net)

How do I get the the top level child nodes(unknownA) of root node with XmlReader in .net? Because their names are unknown, ReadToDescendant(string) and ReadToNextSibling(string)won't work.
<root>
<unknownA/>
<unknownA/>
<unknownA>
<unknownB/>
<unknownB/>
</unknownA>
<unknownA/>
<unknownA>
<unknownB/>
<unknownB>
<unknownC/>
<unknownC/>
</unknownB>
</unknownA>
<unknownA/>
</root>
You can walk through the file using XmlReader.Read(), checking the current Depth against the initial depth, until reaching an element end at the initial depth, using the following extension method:
public static class XmlReaderExtensions
{
public static IEnumerable<string> ReadChildElementNames(this XmlReader xmlReader)
{
if (xmlReader == null)
throw new ArgumentNullException();
if (xmlReader.NodeType == XmlNodeType.Element && !xmlReader.IsEmptyElement)
{
var depth = xmlReader.Depth;
while (xmlReader.Read())
{
if (xmlReader.Depth == depth + 1 && xmlReader.NodeType == XmlNodeType.Element)
yield return xmlReader.Name;
else if (xmlReader.Depth == depth && xmlReader.NodeType == XmlNodeType.EndElement)
break;
}
}
}
public static bool ReadToFirstElement(this XmlReader xmlReader)
{
if (xmlReader == null)
throw new ArgumentNullException();
while (xmlReader.NodeType != XmlNodeType.Element)
if (!xmlReader.Read())
return false;
return true;
}
}
Then it could be used as follows:
var xml = GetXml(); // Your XML string
using (var textReader = new StringReader(xml))
using (var xmlReader = XmlReader.Create(textReader))
{
xmlReader.ReadToFirstElement();
var names = xmlReader.ReadChildElementNames().ToArray();
Console.WriteLine(string.Join("\n", names));
}

Creating tree view dynamically according to json text in Winforms

I am building an application that gets in run-time JSON message from external source.
I don't know anything about the structure of the message text.
I want to take this JSON text, render it to a tree view (or something equivalent, UI regarding),
edit this JSON in that tree view that I just dynamically created, and send the text back to the source.
I really don't know where to start..Any suggestions?
private void btn_Convert_MouseClick(object sender, MouseEventArgs e)
{
try
{
string json = rbt_display.Text;
JObject obj = JObject.Parse(json);
tvw_display.Nodes.Clear();
TreeNode parent = Json2Tree(obj);
parent.Text = "Root Object";
tvw_display.Nodes.Add(parent);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "ERROR");
}
}
private TreeNode Json2Tree(JObject obj)
{
//create the parent node
TreeNode parent = new TreeNode();
//loop through the obj. all token should be pair<key, value>
foreach (var token in obj)
{
//change the display Content of the parent
parent.Text = token.Key.ToString();
//create the child node
TreeNode child = new TreeNode();
child.Text = token.Key.ToString();
//check if the value is of type obj recall the method
if (token.Value.Type.ToString() == "Object")
{
// child.Text = token.Key.ToString();
//create a new JObject using the the Token.value
JObject o = (JObject)token.Value;
//recall the method
child = Json2Tree(o);
//add the child to the parentNode
parent.Nodes.Add(child);
}
//if type is of array
else if (token.Value.Type.ToString() == "Array")
{
int ix = -1;
// child.Text = token.Key.ToString();
//loop though the array
foreach (var itm in token.Value)
{
//check if value is an Array of objects
if (itm.Type.ToString() == "Object")
{
TreeNode objTN = new TreeNode();
//child.Text = token.Key.ToString();
//call back the method
ix++;
JObject o = (JObject)itm;
objTN = Json2Tree(o);
objTN.Text = token.Key.ToString() + "[" + ix + "]";
child.Nodes.Add(objTN);
//parent.Nodes.Add(child);
}
//regular array string, int, etc
else if(itm.Type.ToString() == "Array")
{
ix++;
TreeNode dataArray = new TreeNode();
foreach (var data in itm)
{
dataArray.Text = token.Key.ToString() + "[" + ix + "]";
dataArray.Nodes.Add(data.ToString());
}
child.Nodes.Add(dataArray);
}
else
{
child.Nodes.Add(itm.ToString());
}
}
parent.Nodes.Add(child);
}
else
{
//if token.Value is not nested
// child.Text = token.Key.ToString();
//change the value into N/A if value == null or an empty string
if (token.Value.ToString() == "")
child.Nodes.Add("N/A");
else
child.Nodes.Add(token.Value.ToString());
parent.Nodes.Add(child);
}
}
return parent;
}
sample json
{
"firstName": "John",
"lastName": "Smith",
"isAlive": true,
"age": 25,
"height_cm": 167.6,
"address": {
"streetAddress": "21 2nd Street",
"city": "New York",
"state": "NY",
"postalCode": "10021-3100"
},
"phoneNumbers": [
{
"type": "home",
"number": "212 555-1234"
},
{
"type": "office",
"number": "646 555-4567"
}
],
"children": [],
"spouse": null
}
Note: This example uses NewtonSoft Json. Right-click solution and click manage NuGet packages to install the reference.
This code will handle both JArray or JObject as an input:
string jsonString = "your json string here";
string rootName = "root", nodeName = "node";
JContainer json;
try {
if (jsonString.StartsWith("["))
{
json = JArray.Parse(jsonString);
treeView1.Nodes.Add(Utilities.Json2Tree((JArray)json, rootName, nodeName));
}
else
{
json = JObject.Parse(jsonString);
treeView1.Nodes.Add(Utilities.Json2Tree((JObject)json, text));
}
}
catch(JsonReaderException jre)
{
MessageBox.Show("Invalid Json.");
}
public class Utilities
{
public static TreeNode Json2Tree(JArray root, string rootName = "", string nodeName="")
{
TreeNode parent = new TreeNode(rootName);
int index = 0;
foreach(JToken obj in root)
{
TreeNode child = new TreeNode(string.Format("{0}[{1}]", nodeName, index++));
foreach (KeyValuePair<string, JToken> token in (JObject)obj)
{
switch (token.Value.Type)
{
case JTokenType.Array:
case JTokenType.Object:
child.Nodes.Add(Json2Tree((JObject)token.Value, token.Key));
break;
default:
child.Nodes.Add(GetChild(token));
break;
}
}
parent.Nodes.Add(child);
}
return parent;
}
public static TreeNode Json2Tree(JObject root, string text = "")
{
TreeNode parent = new TreeNode(text);
foreach (KeyValuePair<string, JToken> token in root)
{
switch (token.Value.Type)
{
case JTokenType.Object:
parent.Nodes.Add(Json2Tree((JObject)token.Value, token.Key));
break;
case JTokenType.Array:
int index = 0;
foreach(JToken element in (JArray)token.Value)
{
parent.Nodes.Add(Json2Tree((JObject)element, string.Format("{0}[{1}]", token.Key, index++)));
}
if (index == 0) parent.Nodes.Add(string.Format("{0}[ ]", token.Key)); //to handle empty arrays
break;
default:
parent.Nodes.Add(GetChild(token));
break;
}
}
return parent;
}
private static TreeNode GetChild(KeyValuePair<string, JToken> token)
{
TreeNode child = new TreeNode(token.Key);
child.Nodes.Add(string.IsNullOrEmpty(token.Value.ToString()) ? "n/a" : token.Value.ToString());
return child;
}
}
You can try this code :
public class JsonTag
{
public JsonTag(JsonReader reader)
{
TokenType = reader.TokenType;
Value = reader.Value;
ValueType = reader.ValueType;
}
public JsonToken TokenType { get; set; }
public object Value { get; set; }
public Type ValueType { get; set; }
}
private void JsonToTreeview(string json)
{
tvwValue.BeginUpdate();
var parentText = string.Empty;
TreeNodeCollection parentNodes = tvwValue.Nodes;
TreeNode current = null;
tvwValue.Nodes.Clear();
var reader = new JsonTextReader(new StringReader(json));
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.None:
break;
case JsonToken.StartObject:
current = new TreeNode("{}") { Tag = new JsonTag(reader) };
parentNodes.Add(current);
parentNodes = current.Nodes;
break;
case JsonToken.StartArray:
current = new TreeNode("[]") { Tag = new JsonTag(reader) };
parentNodes.Add(current);
if (current.PrevNode != null)
{
if (((JsonTag)current.PrevNode.Tag).TokenType == JsonToken.PropertyName)
current.Parent.Text += "[]";
parentText = current.Parent.Text;
if (current.Parent.Parent.Text.Length > 2)
parentText = ", " + parentText;
current.Parent.Parent.Text = current.Parent.Parent.Text.Insert(current.Parent.Parent.Text.Length - 1, parentText);
}
parentNodes = current.Nodes;
break;
case JsonToken.StartConstructor:
break;
case JsonToken.PropertyName:
current = new TreeNode("\"" + reader.Value + "\" : ");
parentNodes.Add(current);
if (current.PrevNode != null)
current.PrevNode.Text += ",";
parentNodes = current.Nodes;
current = new TreeNode(reader.Value.ToString()) { Tag = new JsonTag(reader) };
parentNodes.Add(current);
break;
case JsonToken.Comment:
break;
case JsonToken.Raw:
break;
case JsonToken.Date:
case JsonToken.Integer:
case JsonToken.Float:
case JsonToken.Boolean:
case JsonToken.String:
var readerValue = "";
if (reader.TokenType == JsonToken.String)
readerValue = "\"" + reader.Value + "\"";
else
readerValue = reader.Value.ToString();
current = new TreeNode(readerValue) { Tag = new JsonTag(reader) };
parentNodes.Add(current);
current.Parent.Text += readerValue;
parentText = current.Parent.Text;
if (current.Parent.Parent.Text.Length > 2)
parentText = ", " + parentText;
current.Parent.Parent.Text = current.Parent.Parent.Text.Insert(current.Parent.Parent.Text.Length - 1, parentText);
if (((JsonTag)current.PrevNode.Tag).TokenType == JsonToken.PropertyName)
current = current.Parent;
current = current.Parent;
parentNodes = current.Nodes;
break;
case JsonToken.Bytes:
break;
case JsonToken.Null:
break;
case JsonToken.Undefined:
break;
case JsonToken.EndObject:
if (current.FirstNode.Tag != null &&
((JsonTag)current.FirstNode.Tag).TokenType == JsonToken.PropertyName)
current = current.Parent;
current = current.Parent;
if (current == null)
parentNodes = tvwValue.Nodes;
else
parentNodes = current.Nodes;
break;
case JsonToken.EndArray:
if (((JsonTag)current.PrevNode.Tag).TokenType == JsonToken.PropertyName)
current = current.Parent;
current = current.Parent;
if (current == null)
parentNodes = tvwValue.Nodes;
else
parentNodes = current.Nodes;
break;
case JsonToken.EndConstructor:
break;
default:
throw new ArgumentOutOfRangeException();
}
}
tvwValue.EndUpdate();
}
Lots of questions there, really. If you really need guidance on every part of that then it's a lot to try and answer here.
There are classes for reading JSON structures, readily available. As Yosi indirectly linked, there's JSON.net
Once you can read the JSON, you can use it to construct the TreeView
Editing is simple enough, as the TreeView has a property for LabelEdit that supports editing in-place. From there, it's just a matter of reacting to that and keeping track of the changes. Or perhaps reading it all back out in one fell swoop at the end, your choice. Either way, the TreeView has events such as BeforeLabelEdit, AfterLabelEdit, etc., all of which can be found on the TreeView link above.
From the package manager console:
PM> Install-Package Newtonsoft.Json
Then cleaning up #vinceg 's answer, I rolled a static class:
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Windows.Forms;
public static class clsJSON2treeview
{
/// <summary>Parse JSON string, individual tokens become TreeView Nodes ~mwr</summary>
/// <param name="oTV">TreeView control to display parsed JSON</param>
/// <param name="sJSON">Incoming JSON string</param>
/// <param name="rootName">Title of top node in TreeView wrapping all JSON</param>
public static void JsonToTreeview(TreeView oTV, string sJSON, string rootName)
{
JContainer json = sJSON.StartsWith("[")
? (JContainer)JArray.Parse(sJSON)
: (JContainer)JObject.Parse(sJSON);
oTV.Nodes.Add(Ele2Node(json, rootName));
}
private static TreeNode Ele2Node(object oJthingy, string text = "")
{
TreeNode oThisNode = new TreeNode(text);
switch (oJthingy.GetType().Name) //~mwr could not find parent object for all three JObject, JArray, JValue
{
case "JObject":
foreach (KeyValuePair<string, JToken> oJtok in (JObject)oJthingy)
oThisNode.Nodes.Add(Ele2Node(oJtok.Value, oJtok.Key));
break;
case "JArray":
int i = 0;
foreach (JToken oJtok in (JArray)oJthingy)
oThisNode.Nodes.Add(Ele2Node(oJtok, string.Format("[{0}]", i++)));
if (i == 0) oThisNode.Nodes.Add("[]"); //to handle empty arrays
break;
case "JValue":
oThisNode.Nodes.Add(new TreeNode(oJthingy.ToString()));
break;
default:
throw new System.Exception("clsJSON2Treeview can't interpret object:" + oJthingy.GetType().Name);
}
return oThisNode;
}
}

XML take the position of an element and at next usage go directly there

So i have a huge XML file ( wikipedia dump xml ) .
My school project requirement says that i should be able to do a really fast search on this xml file ( so no, not import it into an sql database )
so of course i want to create an indexer, that will display into a separate file ( probably xml ) something like this : [content to search]:[byte offset to the start of the xml node that contains the content]
My question is how can i take the position of the element, and how can I jump to that position in the xml in case it is required for a search ?
The project is in C#. Thank you in advance.
Later Edit : I am trying to work with XmlReader, but I am open for any other suggestions.
For the moment this is how I read my XML for a non-indexed search
XmlReader reader = XmlReader.Create(FileName);
while (reader.Read())
{
switch (reader.Name)
{
case "page":
Boolean found = false;
String title = "";
String element = "<details>";
readMore(reader, "title");
title = reader.Value;
if (title.Contains(word))
{
found = true;
}
readMore(reader, "text");
String content = reader.Value;
if (content.Contains(word) & !found)
{
found = true;
}
if (found)
{
element += "<summary>" + title + " (click)</summary>";
element += content;
element += "</details>";
result.Add(element);
}
break;
}
}
reader.Close();
if (result.Count == 0)
{
result.Add("No results were found");
}
return result;
…
static void readMore(XmlReader reader, String name)
{
while (reader.Name != name)
{
reader.Read();
}
reader.Read();
}
The correct solution would be to use an intermediary binary format; but if you can't do that, and assuming that you use DOM, I don't see any solution but to store the position of the node in the DOM tree as a list of indexes.
Example in JavaScript (should be fairly the same in C#):
function getPosition(node) {
var pos = [], i = 0;
while (node != document.documentElement) {
if (node.previousSibling) {
++i;
node = node.previousSibling;
} else {
pos.unshift(i);
i = 0;
node = node.parentNode;
}
}
return pos;
}
function getNode(pos) {
var node = document.documentElement;
for (var i = 0; i < pos.length; ++i) {
node = node.childNodes[pos[i]];
}
return node;
}

get attribute name in addition to attribute value in xml

I am receiving dynamic xml where I won't know the attribute names, if you'll look at the xml and code... I tried to make a simple example, I can get the attribute values i.e. "myName", "myNextAttribute", and "blah", but I can't get the attribute names i.e. "name", "nextAttribute", and "etc1". Any ideas, I figure it has to be something easy I'm missing...but I'm sure missing it.
static void Main(string[] args)
{
string xml = "<test name=\"myName\" nextAttribute=\"myNextAttribute\" etc1=\"blah\"/>";
TextReader sr = new StringReader(xml);
using (XmlReader xr = XmlReader.Create(sr))
{
while (xr.Read())
{
switch (xr.NodeType)
{
case XmlNodeType.Element:
if (xr.HasAttributes)
{
for (int i = 0; i < xr.AttributeCount; i++)
{
System.Windows.Forms.MessageBox.Show(xr.GetAttribute(i));
}
}
break;
default:
break;
}
}
}
}
You can see in MSDN:
if (reader.HasAttributes) {
Console.WriteLine("Attributes of <" + reader.Name + ">");
while (reader.MoveToNextAttribute()) {
Console.WriteLine(" {0}={1}", reader.Name, reader.Value);
}
// Move the reader back to the element node.
reader.MoveToElement();
}
Your switch is unnecessary since you only have a single case, try rolling that into your if statement instead.
if (xr.NodeType && xr.HasAttributes)
{
...
}
Note that the && operator evaluates in order, so if xr.NoteType is false, the rest of the arguments are ignored and the if block is skipped.

Categories

Resources