NodeJS checking if XML element exists and add or delete - node.js

I'm working with XML in NodeJS. I've been using the xmlbuilder to create my XML. The problem is that now I need to check if a element already exists and delete or update it.
For example, I have the following XML
<?xml version="1.0"?>
<listings>
<listing>
<id>1</id>
<name>TEST</name>
<description>TEST</description>
</listing>
<listing>
<id>2</id>
<name>TEST</name>
<description>TEST</description>
</listing>
</listings>
Then, I call my updateXML controller to add data to it.
const builder = require('xmlbuilder');
const fs = require('fs');
const path = require("path");
exports.updateXML = async (req, res, next) => {
const data = req.body.data;
/*
For example data is
{
id: 2,
name: "Test2",
description: "Desc2"
}
*/
const xmlFile = fs.readFileSync(path.resolve(__dirname, "./oodle.xml"), 'utf8');
if(/*How do I check if the xmlFile has a <id> === data.id?*/) {
// If id matches. How can I delete the whole <listing> node for that id?
}
const newListing = builder.create('listing');
newListing.ele("id", data.id);
newListing.ele("name", data.name);
newListing.ele("description", data.description);
// How can I add the newListing node to the xmlFile?
}
Thanks

I don't believe it's really necessary to remove the node with the duplicate <id>, create a new <listing> node and then insert it into the xml. At least as far as the sample xml in your question is concerned, you can just modify the text child nodes of the relevant <listing> node.
Something along the lines of:
const { select } = require('xpath');
let query = `//listing[./id[./text()="${data.id}"]]`;
const nodes = select(query, doc.node);
nodes.forEach(function (node) {
nam = select('.//name/text()',node)
desc = select('.//description/text()',node)
nam[0].data = data.name;
desc[0].data = data.description;
});
const serializedXML = doc.end({ format: 'xml', prettyPrint: true });
console.log(serializedXML)
Output:
<?xml version="1.0"?>
<listings>
<listing>
<id>1</id>
<name>TEST</name>
<description>TEST</description>
</listing>
<listing>
<id>2</id>
<name>Test2</name>
<description>Desc2</description>
</listing>
</listings>

Related

Import multiple Annotations in Pdftron with NodeJs

In my application annotations are stored in the database as single annotations. For one document in the document table I store many annotations (multiple xfdf strings) in the annotation table.
I wrote a code to generate the pdf and import these annotations. I referred following links for this code,
https://www.pdftron.com/documentation/web/guides/features/forms/import-data/
https://groups.google.com/g/pdfnet-sdk/c/gXaG5X-zpR8
params,
annotations : list of annotations with xfdf String
downloadedFile : pdf file as a buffer
isFlatternAnnotations - is a boolean option to
flatten annotations
async importAnnotationsToDocument(
annotations: any,
downloadedFile: any,
isFlatternAnnotations: any,
) {
await PDFNet.initialize();
const pdfDocument = await PDFNet.PDFDoc.createFromBuffer(downloadedFile);
pdfDocument.lock();
let fdfDocument = null;
annotations.forEach(async annotation => {
fdfDocument = await PDFNet.FDFDoc.createFromXFDF(annotation.xfdfString);
await pdfDocument.fdfMerge(fdfDocument);
});
if (isFlatternAnnotations === 'true') {
await pdfDocument.flattenAnnotations();
}
const documentBuffer = await pdfDocument.saveMemoryBuffer(
PDFNet.SDFDoc.SaveOptions.e_remove_unused,
);
const documentBufferResponse = Buffer.from(documentBuffer);
PDFNet.shutdown();
return documentBufferResponse;
}
However I noticed the code is working only the await pdfDocument.flattenAnnotations(); is running. If it is not running annotations are not merged in the document.
And also if it runs a single time, the annotations are displayed without flattening. But if I add the same line three times it works correctly.
I think the way I have done this is not correct. I need your help to write this code correctly.
Following code works correctly, but there should be a proper way to do this.
async importAnnotationsToDocument(
annotations: any,
downloadedFile: any,
isFlatternAnnotations: any,
) {
await PDFNet.initialize();
const pdfDocument = await PDFNet.PDFDoc.createFromBuffer(downloadedFile);
pdfDocument.lock();
let fdfDocument = null;
annotations.forEach(async annotation => {
fdfDocument = await PDFNet.FDFDoc.createFromXFDF(annotation.xfdfString);
await pdfDocument.fdfMerge(fdfDocument);
});
if (isFlatternAnnotations === 'true') {
await pdfDocument.flattenAnnotations();
await pdfDocument.flattenAnnotations();
await pdfDocument.flattenAnnotations();
} else {
// This shows the annotations without flattening
await pdfDocument.flattenAnnotations();
}
const documentBuffer = await pdfDocument.saveMemoryBuffer(
PDFNet.SDFDoc.SaveOptions.e_remove_unused,
);
const documentBufferResponse = Buffer.from(documentBuffer);
PDFNet.shutdown();
return documentBufferResponse;
}
Following is the xfdf String for a single annotation
<?xml version="1.0" encoding="UTF-8"?>
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
<fields />
<add>
<square page="0"
rect="387.88,525.73,525,609.07"
color="#FFCD45" flags="print"
name="d1aa1a2a-822f-507b-6ff6-d61bcc6bd862"
title="test.title" subject="Rectangle"
date="D:20210405104448+08'00'"
interior-color="#FFCD45"
opacity="0.5"
creationdate="D:20210405104445+08'00'" />
</add>
<modify /><delete />
</xfdf>
You are loading a variant of XFDF, called Command, which you can see documented here. https://www.pdftron.com/documentation/web/guides/xfdf/
The following code is how you would load and apply XFDF command XML data to a PDFDoc instance.
let fdfDoc = await pdfDoc.fdfExtract(PDFNet.PDFDoc.ExtractFlag.e_both);
await fdfDoc.saveAsXFDF('data.xfdf');
fdfDoc = await PDFNet.FDFDoc.createFromXFDF('data.xfdf');
await fdfDoc.mergeAnnots(str);
await pdfDoc.fdfUpdate(fdfDoc);
await pdfDoc.refreshAnnotAppearances();
The API pdfDocument.flattenAnnotations() by default only flattens FormFields, and not annotations.
https://www.pdftron.com/api/pdfnet-node/PDFNet.PDFDoc.html#flattenAnnotations__anchor
So I would recommend changing the call to
pdfDocument.flattenAnnotations(false);

Node.js JSON to XML - serialize a complex object

I'm attempting to convert a complex JSON object to XML using this package - XML.
var xml = require("xml");
var obj = { MS : { ts : 3423523, isOk : false , errors : [] }};
var xmlObj = xml(obj); // This outputs <MS/>
Any ideas how to make the XML parser go deeper? Why is it prematurely closed?
You could give the xml2js module a try, this would convert your object to Xml quite easily, e.g.
const xml2js = require('xml2js');
const obj = { MS : { ts : 3423523, isOk : false , errors : [] }};
const builder = new xml2js.Builder( { headless: false, renderOpts: { pretty: true } });
const xml = builder.buildObject(obj);
console.log(xml)
The output would be:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<MS>
<ts>3423523</ts>
<isOk>false</isOk>
</MS>

JSON to XML conversion in Node JS service (using xml2js)

I need some help/advice with JSON to XML conversion in Node js.
I have a service that gets a JSON object in request body that needs to convert to XML. I am able to achieve this using node-xml2js for json inputs with maximum one level of nested objects. But, it gets way more complicated with nested objects having attribute values. Attributes should be identified first, prefixed with $ sign and enclosed in curly braces before parsing through xml2js to get correct xml.
Is there a better way of doing this where this complicated layer of reformatting the json input can be simplified?
xml2js can converts this:
{
"Level1":{ "$":{ "attribute":"value" },
"Level2": {"$":{"attribute1":"05/29/2020",
"attribute2":"10","attribute3":"Pizza"}}
}
to this:(which is correct):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Level1 attribute="value">
<Level2 attribute1="05/29/2020" attribute2="10" attribute3="Pizza"/>
</Level1>
But actual json input is this:
{
"Level1":{"attribute":"value",
"Level2": {"attribute1":"05/29/2020",
"attribute2":"10","attribute3":"Pizza"} }
}
Expected same result:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Level1 attribute="value">
<Level2 attribute1="05/29/2020" attribute2="10" attribute3="Pizza"/>
</Level1>
Please let me know if you have worked on similar requirements. Appreciate any help.
Thank you.
This would be a way to change the object back to the format expected in the library, although it assumes that all non object keys are supposed to be attributes (is that a valid assumption for your application?)
function groupChildren(obj) {
for(prop in obj) { // consider filtering for own properties (vs from prototype: for(prop of Object.keys(obj)) {
if (typeof obj[prop] === 'object') {
groupChildren(obj[prop]);
} else {
obj['$'] = obj['$'] || {};
obj['$'][prop] = obj[prop];
delete obj[prop];
}
}
return obj;
}
and then used like so:
var xml2js = require('xml2js');
var obj = {
Level1: {
attribute: 'value',
Level2: {
attribute1: '05/29/2020',
attribute2: '10',
attribute3: 'Pizza'
}
}
};
var builder = new xml2js.Builder();
var xml = builder.buildObject(groupChildren(obj));
which prints out:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Level1 attribute="value">
<Level2 attribute1="05/29/2020" attribute2="10" attribute3="Pizza"/>
</Level1>
you can use this library :nashwaan/xml-js
Like This:
let xmlJ=require('xml-js');
let parseToJson=(xml)=>{
return new Promise(resolve => {
let convert;
convert=xmlJ.xml2json(xml,{compact:true});
resolve(convert);
});
};

Add multiple worksheet to a stream workbook with ExcelJS

Context
NodeJS 10
ExcelJS 0.8.5
LibreOffice 5.1.6.2 on Ubuntu
Issue
I am trying to create a multi-sheet Excel file with ExcelJS.
I am following the official documentation from the ExcelJS github page.
The first step is the creation of the workbook. In my case, i want a stream because i will append lot of datas.
// Create Excel Workbook Stream
const workbookStream = new Excel.stream.xlsx.WorkbookWriter({
filename: path,
useStyles: true,
useSharedStrings: true,
});
Then i add sheet to the created workbook's stream as said into the documentation Worksheet Properties.
const sheet = workbookStream.addSheet('sheet1'); // Throw here
But in this way, i got the following error :
'Type error: workbookStream.addSheet is not a function
I have also found a code that do not throw but do not work and do not create many sheets.
const header = ['A', 'B', 'C'];
const sheet1 = Excel.addSheetOnWorkbook({
workbook: workbookStream,
name: 'sheet1',
});
const sheet2 = Excel.addSheetOnWorkbook({
workbook: workbookStream,
name: 'sheet2',
});
sheet.addRow(header).commit();
sheet.addRow(header).commit();
await workbookStream.commit();
In this case, only the sheet1 is created (opening with LibreOffice 5.1.6.2).
Any way to resolve this case with ExcelJS ?
I don't know how much help this is to you, but on my environment:
Windows 10 (10.0.17134.0)
Node 10.15.0
exceljs 1.6.0
Once I set the worksheet.state to visible, I can see it in LibreOffice:
const Excel = require('exceljs');
const workbook = new Excel.Workbook();
const fs = require('fs');
const filename = "test.xlsx";
const sheetNames = ["A", "B", "C", "D"];
sheetNames.forEach(sheetName => {
let worksheet = workbook.addWorksheet(sheetName);
// I believe this needs to be set to show in LibreOffice.
worksheet.state = 'visible';
});
const stream = fs.createWriteStream(filename);
workbook.xlsx.write(stream)
.then(function() {
console.log(`File: ${filename} saved!`);
stream.end();
}).catch(error => {
console.err(`File: ${filename} save failed: `, error);
});
Using the Streaming XLSX WorkbookWriter:
const Excel = require('exceljs');
const sheetNames = ["A", "B", "C", "D"];
const workbook = new Excel.stream.xlsx.WorkbookWriter( { filename: './streamed-workbook.xlsx' } );
sheetNames.forEach(sheetName => {
let worksheet = workbook.addWorksheet(sheetName);
worksheet.state = 'visible';
worksheet.commit();
});
// Finished the workbook.
workbook.commit()
.then(function() {
console.log(`Worksheet committed!`);
});
I'll test on an Ubuntu machine as well.
XSLX files are simply .zip files containing multiple .xml files, so you can inspect the XML data yourself.
To show the raw Xml (worksheet.xml) produced by LibreOffice versus exceljs:
LibreOffice
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<fileVersion appName="Calc"/>
<workbookPr backupFile="false" showObjects="all" date1904="false"/>
<workbookProtection/>
<bookViews>
<workbookView showHorizontalScroll="true" showVerticalScroll="true" showSheetTabs="true" xWindow="0" yWindow="0" windowWidth="16384" windowHeight="8192" tabRatio="500" firstSheet="0" activeTab="1"/>
</bookViews>
<sheets>
<sheet name="A" sheetId="1" state="visible" r:id="rId2"/>
<sheet name="B" sheetId="2" state="visible" r:id="rId3"/>
</sheets>
<calcPr iterateCount="100" refMode="A1" iterate="false" iterateDelta="0.001"/>
<extLst>
<ext xmlns:loext="http://schemas.libreoffice.org/" uri="{7626C862-2A13-11E5-B345-FEFF819CDC9F}">
<loext:extCalcPr stringRefSyntax="CalcA1"/>
</ext>
</extLst>
</workbook>
exceljs
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x15" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main">
<fileVersion appName="xl" lastEdited="5" lowestEdited="5" rupBuild="9303"/>
<workbookPr defaultThemeVersion="164011" filterPrivacy="1"/>
<sheets>
<sheet sheetId="1" name="A" state="show" r:id="rId3"/>
<sheet sheetId="2" name="B" state="show" r:id="rId4"/>
<sheet sheetId="3" name="C" state="show" r:id="rId5"/>
<sheet sheetId="4" name="D" state="show" r:id="rId6"/>
</sheets>
<calcPr calcId="171027"/>
</workbook>
One more thing
The sheet title can corrupt the XML file structure when some non alphanum characters are set.
Be careful about sheet title !

Extracting Extended Data from KML using Javascript

I have a placemark in a KML that looks something like this
<Placemark>
<id>test345</id>
<name>Images from KML file</name>
<ExtendedData>
<Data name="type">
<value>images</value>
</Data>
</ExtendedData>
<Point>
<coordinates>-122.448425,37.802907,0</coordinates>
</Point>
I'm attempting to extract the ExtendedData information out of this placemarker on a click event:
google.earth.addEventListener(kmlObject, 'click', function(event) {
event.preventDefault();
var kmlPlacemark = event.getTarget();
});
An alternative solution would be to get the kmlObject from the kmlPlacemarker, any ideas?
Given the placemark the Google Earth API provides two methods to access the ExtendedData element.
getBalloonHtml()
getBalloonHtmlUnsafe()
API Reference:
https://developers.google.com/earth/documentation/reference/interface_kml_feature
You can find a working example in the Google Code Playground here:
https://code.google.com/apis/ajax/playground/?exp=earth#extended_data_in_balloons
If you wanted to get the raw KML for extended data then you could fetch the KML representation and parse it as an XML document.
var output = placemark.getKml();
Just to say I posted about just this issue on the support forum for the plug-in: https://code.google.com/p/earth-api-samples/issues/detail?id=16
Here is a method I cobbled together to provide support for getExtendedData. It takes a string of Kml as the argument via 'feature.getKml();` It returns any extended data elements that have values in a key[value] object. It expects the extended data to be in the format:
<Data name="Foo">
<value>bar</value>
</Data>
Tested in XP - FF3.0, IE7, Chrome
function getExtendedData(kmlString) {
var xmlDoc = null;
var keyValue = [];
//Parse the kml
try {
//Internet Explorer
xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
xmlDoc.async="false";
xmlDoc.loadXML(kmlString);
} catch(e) {
try {
//Firefox, etc.
var parser = new DOMParser();
xmlDoc = parser.parseFromString(kmlString,"text/xml");
}
catch(e) {
//Failed to parse
alert(e.message);
return;
}
}
// Get all the named elements
var data = xmlDoc.getElementsByTagName("Data");
// Iterate through the data elements
for(var i=0; i<data.length; i++) {
if(data[i].getAttribute("name") &&
data[i].getElementsByTagName("value").length > 0) {
// Get the name and value
var name = data[i].getAttribute("name");
var value = data[i].getElementsByTagName("value")[0].firstChild.data;
// Assign them to the keyValue object
keyValue[name] = value;
}
}
return keyValue;
}
Usage
// where 'feature' is the object with the extended data
var data = getExtendedData(feature.getKml());
for (var name in data) {
var value = data[name];
alert(name + '=' + value); // e.g. type=images
}
It is actually possible to access the ExtendedData elements via the DOM APIs, although they're not particularly well-documented anywhere.
I found them while grepping around inside some of the resource (.rcc) files packaged with the Plugin.
Assuming a simple Placemark sample similar to yours:
<Placemark id="testmark">
<!-- other stuff... -->
<ExtendedData>
<Data name="someDataUrl">
<displayName>URL Representing some Data</displayName>
<value>http://example.com/#hello</value>
</Data>
</ExtendedData>
</Placemark>
Then (once it's fetched/parsed/loaded into Earth, you can access it something like:
var mark = ge.getElementById('testmark');
var extDataObj = mark.getExtendedData();
var extDataOut = Array(extDataObj.getDataCount());
for (var i = 0; i < extDataObj.getDataCount(); i++) {
var item = extDataObj.getData(i);
var details = { name: item.getName(),
displayName: item.getDisplayName(),
value: item.getValue()
};
extDataOut[i] = details;
}
console.dir(extDataOut);
Haven't tested it for performance vs the .getKml() and feed to an external parser approach, and the lack of official documentation might mean it's not fully functional or supported, but in all testing so far it seems to do ok. I haven't yet found a way to access any of the more complicated SchemaData type structures, only the simple <data name=''><value>... form.

Resources