Skip to content

Commit 94f7443

Browse files
committed
Polish xml merge
1 parent 46aa1b6 commit 94f7443

File tree

5 files changed

+196
-8
lines changed

5 files changed

+196
-8
lines changed

lib/definitions/platform.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ interface IPlatformData {
3636
targetedOS?: string[];
3737
configurationFileName?: string;
3838
configurationFilePath?: string;
39+
mergeXmlConfig?: any[];
3940
}
4041

4142
interface IPlatformsData {

lib/services/android-project-service.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ class AndroidProjectService implements IPlatformProjectService {
4848
],
4949
frameworkFilesExtensions: [".jar", ".dat", ".so"],
5050
configurationFileName: "AndroidManifest.xml",
51-
configurationFilePath: path.join(this.$projectData.platformsDir, "android", "AndroidManifest.xml")
51+
configurationFilePath: path.join(this.$projectData.platformsDir, "android", "AndroidManifest.xml"),
52+
mergeXmlConfig: [{ "nodename": "manifest", "attrname": "*" }]
5253
};
5354
}
5455

lib/services/plugins-service.ts

+28-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import semver = require("semver");
66
import Future = require("fibers/future");
77
import constants = require("./../constants");
88
let xmlmerge = require("xmlmerge-js");
9+
let DOMParser = require('xmldom').DOMParser;
910

1011
export class PluginsService implements IPluginsService {
1112
private static INSTALL_COMMAND_NAME = "install";
@@ -77,12 +78,22 @@ export class PluginsService implements IPluginsService {
7778
shelljs.cp("-R", pluginData.fullPath, pluginDestinationPath);
7879

7980
let pluginPlatformsFolderPath = path.join(pluginDestinationPath, pluginData.name, "platforms", platform);
80-
let pluginConfigurationFilePath = path.join(pluginPlatformsFolderPath, platformData.configurationFileName);
81+
let pluginConfigurationFilePath = path.join(pluginPlatformsFolderPath, platformData.configurationFileName);
82+
let configurationFilePath = platformData.configurationFilePath;
83+
8184
if(this.$fs.exists(pluginConfigurationFilePath).wait()) {
85+
// Validate plugin configuration file
8286
let pluginConfigurationFileContent = this.$fs.readText(pluginConfigurationFilePath).wait();
83-
let configurationFileContent = this.$fs.readText(platformData.configurationFilePath).wait();
84-
let resultXml = this.mergeXml(pluginConfigurationFileContent, configurationFileContent).wait();
85-
this.$fs.writeFile(platformData.configurationFilePath, resultXml).wait();
87+
this.validateXml(pluginConfigurationFileContent, pluginConfigurationFilePath);
88+
89+
// Validate configuration file
90+
let configurationFileContent = this.$fs.readText(configurationFilePath).wait();
91+
this.validateXml(configurationFileContent, configurationFilePath);
92+
93+
// Merge xml
94+
let resultXml = this.mergeXml(configurationFileContent, pluginConfigurationFileContent, platformData.mergeXmlConfig || []).wait();
95+
this.validateXml(resultXml);
96+
this.$fs.writeFile(configurationFilePath, resultXml).wait();
8697
}
8798

8899
if(this.$fs.exists(pluginPlatformsFolderPath).wait()) {
@@ -209,11 +220,11 @@ export class PluginsService implements IPluginsService {
209220
}).future<string>()();
210221
}
211222

212-
private mergeXml(xml1: string, xml2: string): IFuture<string> {
223+
private mergeXml(xml1: string, xml2: string, config: any[]): IFuture<string> {
213224
let future = new Future<string>();
214225

215226
try {
216-
xmlmerge.merge(xml1, xml2, "", (mergedXml: string) => {
227+
xmlmerge.merge(xml1, xml2, config, (mergedXml: string) => {
217228
future.return(mergedXml);
218229
});
219230
} catch(err) {
@@ -222,5 +233,16 @@ export class PluginsService implements IPluginsService {
222233

223234
return future;
224235
}
236+
237+
private validateXml(xml: string, xmlFilePath?: string): void {
238+
let doc = new DOMParser({
239+
locator: {},
240+
errorHandler: (level: any, msg: string) => {
241+
let errorMessage = xmlFilePath ? `Invalid xml file ${xmlFilePath}.` : `Invalid xml ${xml}.`;
242+
this.$errors.fail(errorMessage + ` Additional technical information: ${msg}.` )
243+
}
244+
});
245+
doc.parseFromString(xml, 'text/xml');
246+
}
225247
}
226248
$injector.register("pluginsService", PluginsService);

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"winreg": "0.0.12",
6868
"ws": "0.7.1",
6969
"xcode": "https://github.com/NativeScript/node-xcode/archive/NativeScript-0.9.tar.gz",
70+
"xmldom": "0.1.19",
7071
"xmlhttprequest": "https://github.com/telerik/node-XMLHttpRequest/tarball/master",
7172
"xmlmerge-js": "0.2.4",
7273
"yargs": "1.2.2"

test/plugins-service.ts

+164-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import ErrorsLib = require("../lib/common/errors");
1616
import ProjectHelperLib = require("../lib/common/project-helper");
1717
import PlatformsDataLib = require("../lib/platforms-data");
1818
import ProjectDataServiceLib = require("../lib/services/project-data-service");
19+
import helpers = require("../lib/common/helpers");
20+
import os = require("os");
1921

2022
import PluginsServiceLib = require("../lib/services/plugins-service");
2123
import AddPluginCommandLib = require("../lib/commands/plugin/add-plugin");
@@ -124,6 +126,30 @@ function addPluginWhenExpectingToFail(testInjector: IInjector, plugin: string, e
124126
assert.isTrue(isErrorThrown);
125127
}
126128

129+
function createAndroidManifestFile(projectFolder: string, fs:IFileSystem): void {
130+
let manifest = '<?xml version="1.0" encoding="UTF-8"?>' +
131+
'<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.basiccontactables" android:versionCode="1" android:versionName="1.0" >' +
132+
'<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>' +
133+
'<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>' +
134+
'<uses-permission android:name="android.permission.INTERNET"/>' +
135+
'<application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/Theme.Sample" >' +
136+
'<activity android:name="com.example.android.basiccontactables.MainActivity" android:label="@string/app_name" android:launchMode="singleTop">' +
137+
'<meta-data android:name="android.app.searchable" android:resource="@xml/searchable" />' +
138+
'<intent-filter>' +
139+
'<action android:name="android.intent.action.SEARCH" />' +
140+
'</intent-filter>' +
141+
'<intent-filter>' +
142+
'<action android:name="android.intent.action.MAIN" />' +
143+
'</intent-filter>' +
144+
'</activity>' +
145+
'</application>' +
146+
'</manifest>';
147+
148+
fs.createDirectory(path.join(projectFolder, "platforms")).wait();
149+
fs.createDirectory(path.join(projectFolder, "platforms", "android")).wait();
150+
fs.writeFile(path.join(projectFolder, "platforms", "android", "AndroidManifest.xml"), manifest).wait();
151+
}
152+
127153
describe("Plugins service", () => {
128154
let testInjector: IInjector;
129155
beforeEach(() => {
@@ -256,7 +282,7 @@ describe("Plugins service", () => {
256282
let packageJsonContent = fs.readJson(path.join(projectFolder, "package.json")).wait();
257283
let actualDependencies = packageJsonContent.dependencies;
258284
let expectedDependencies = {
259-
"plugin1": "^1.0.0"
285+
"plugin1": "^1.0.3"
260286
};
261287
assert.deepEqual(actualDependencies, expectedDependencies);
262288
});
@@ -419,4 +445,141 @@ describe("Plugins service", () => {
419445
commandsService.tryExecuteCommand("plugin|add", [pluginFolderPath]).wait();
420446
});
421447
});
448+
449+
describe("merge xmls tests", () => {
450+
let testInjector: IInjector;
451+
beforeEach(() => {
452+
testInjector = createTestInjector();
453+
testInjector.registerCommand("plugin|add", AddPluginCommandLib.AddPluginCommand);
454+
});
455+
it("fails if the plugin contains incorrect xml", () => {
456+
let pluginName = "mySamplePlugin";
457+
let projectFolder = createProjectFile(testInjector);
458+
let pluginFolderPath = path.join(projectFolder, pluginName);
459+
let pluginJsonData = {
460+
"name": pluginName,
461+
"version": "0.0.1",
462+
"nativescript": {
463+
"platforms": {
464+
"android": "0.10.0"
465+
}
466+
}
467+
};
468+
let fs = testInjector.resolve("fs");
469+
fs.writeJson(path.join(pluginFolderPath, "package.json"), pluginJsonData).wait();
470+
471+
// Adds AndroidManifest.xml file in platforms/android folder
472+
createAndroidManifestFile(projectFolder, fs);
473+
474+
// Mock plugins service
475+
let pluginsService = testInjector.resolve("pluginsService");
476+
pluginsService.getAllInstalledPlugins = () => {
477+
return (() => {
478+
return [{
479+
name: ""
480+
}];
481+
}).future<IPluginData[]>()();
482+
}
483+
484+
let appDestinationDirectoryPath = path.join(projectFolder, "platforms", "android");
485+
486+
// Mock platformsData
487+
let platformsData = testInjector.resolve("platformsData");
488+
platformsData.getPlatformData = (platform: string) => {
489+
return {
490+
appDestinationDirectoryPath: appDestinationDirectoryPath,
491+
frameworkPackageName: "tns-android",
492+
configurationFileName: "AndroidManifest.xml"
493+
}
494+
}
495+
496+
// Ensure the pluginDestinationPath folder exists
497+
let pluginPlatformsDirPath = path.join(appDestinationDirectoryPath, "app", "tns_modules", pluginName, "platforms", "android");
498+
fs.ensureDirectoryExists(pluginPlatformsDirPath).wait();
499+
500+
// Creates invalid plugin's AndroidManifest.xml file
501+
let xml = '<?xml version="1.0" encoding="UTF-8"?>' +
502+
'<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.basiccontactables" android:versionCode="1" android:versionName="1.0" >' +
503+
'<uses-permission android:name="android.permission.READ_CONTACTS"/>';
504+
let pluginConfigurationFilePath = path.join(pluginPlatformsDirPath, "AndroidManifest.xml");
505+
fs.writeFile(pluginConfigurationFilePath, xml).wait();
506+
507+
// Expected error message. The assertion happens in mockBeginCommand
508+
let expectedErrorMessage = `Exception: Invalid xml file ${pluginConfigurationFilePath}. Additional technical information: element parse error: Exception: Invalid xml file ` +
509+
`${pluginConfigurationFilePath}. Additional technical information: unclosed xml attribute` +
510+
`\n@#[line:1,col:39].` +
511+
`\n@#[line:1,col:39].`;
512+
mockBeginCommand(testInjector, expectedErrorMessage);
513+
514+
let commandsService = testInjector.resolve(CommandsServiceLib.CommandsService);
515+
commandsService.tryExecuteCommand("plugin|add", [pluginFolderPath]).wait();
516+
});
517+
it("merges AndroidManifest.xml and produces correct xml", () => {
518+
let pluginName = "mySamplePlugin";
519+
let projectFolder = createProjectFile(testInjector);
520+
let pluginFolderPath = path.join(projectFolder, pluginName);
521+
let pluginJsonData = {
522+
"name": pluginName,
523+
"version": "0.0.1",
524+
"nativescript": {
525+
"platforms": {
526+
"android": "0.10.0"
527+
}
528+
}
529+
};
530+
let fs = testInjector.resolve("fs");
531+
fs.writeJson(path.join(pluginFolderPath, "package.json"), pluginJsonData).wait();
532+
533+
// Adds AndroidManifest.xml file in platforms/android folder
534+
createAndroidManifestFile(projectFolder, fs);
535+
536+
// Mock plugins service
537+
let pluginsService = testInjector.resolve("pluginsService");
538+
pluginsService.getAllInstalledPlugins = () => {
539+
return (() => {
540+
return [{
541+
name: ""
542+
}];
543+
}).future<IPluginData[]>()();
544+
}
545+
546+
let appDestinationDirectoryPath = path.join(projectFolder, "platforms", "android");
547+
548+
// Mock platformsData
549+
let platformsData = testInjector.resolve("platformsData");
550+
platformsData.getPlatformData = (platform: string) => {
551+
return {
552+
appDestinationDirectoryPath: appDestinationDirectoryPath,
553+
frameworkPackageName: "tns-android",
554+
configurationFileName: "AndroidManifest.xml",
555+
configurationFilePath: path.join(appDestinationDirectoryPath, "AndroidManifest.xml"),
556+
mergeXmlConfig: [{ "nodename": "manifest", "attrname": "*" }]
557+
}
558+
}
559+
560+
// Ensure the pluginDestinationPath folder exists
561+
let pluginPlatformsDirPath = path.join(appDestinationDirectoryPath, "app", "tns_modules", pluginName, "platforms", "android");
562+
fs.ensureDirectoryExists(pluginPlatformsDirPath).wait();
563+
564+
// Creates valid plugin's AndroidManifest.xml file
565+
let xml = '<?xml version="1.0" encoding="UTF-8"?>' +
566+
'<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.basiccontactables" android:versionCode="1" android:versionName="1.0" >' +
567+
'<uses-permission android:name="android.permission.READ_CONTACTS"/>' +
568+
'</manifest>';
569+
let pluginConfigurationFilePath = path.join(pluginPlatformsDirPath, "AndroidManifest.xml");
570+
fs.writeFile(pluginConfigurationFilePath, xml).wait();
571+
572+
pluginsService.add(pluginFolderPath).wait();
573+
574+
let expectedXml = '<?xmlversion="1.0"encoding="UTF-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android"package="com.example.android.basiccontactables"android:versionCode="1"android:versionName="1.0"><uses-permissionandroid:name="android.permission.READ_EXTERNAL_STORAGE"/><uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/><uses-permissionandroid:name="android.permission.INTERNET"/><applicationandroid:allowBackup="true"android:icon="@drawable/ic_launcher"android:label="@string/app_name"android:theme="@style/Theme.Sample"><activityandroid:name="com.example.android.basiccontactables.MainActivity"android:label="@string/app_name"android:launchMode="singleTop"><meta-dataandroid:name="android.app.searchable"android:resource="@xml/searchable"/><intent-filter><actionandroid:name="android.intent.action.SEARCH"/></intent-filter><intent-filter><actionandroid:name="android.intent.action.MAIN"/></intent-filter></activity></application><uses-permissionandroid:name="android.permission.READ_CONTACTS"/></manifest>';
575+
expectedXml = helpers.stringReplaceAll(expectedXml, os.EOL, "");
576+
expectedXml = helpers.stringReplaceAll(expectedXml, " ", "");
577+
578+
let actualXml = fs.readText(path.join(appDestinationDirectoryPath, "AndroidManifest.xml")).wait();
579+
actualXml = helpers.stringReplaceAll(actualXml, "\n", "");
580+
actualXml = helpers.stringReplaceAll(actualXml, " ", "");
581+
582+
assert.equal(expectedXml, actualXml);
583+
});
584+
});
422585
});

0 commit comments

Comments
 (0)