diff --git a/deploy/lib/xquery/setup.xqy b/deploy/lib/xquery/setup.xqy
index ab471700..fe0508ac 100644
--- a/deploy/lib/xquery/setup.xqy
+++ b/deploy/lib/xquery/setup.xqy
@@ -404,6 +404,13 @@ declare function setup:get-rollback-config()
{
element configuration
{
+ element gr:task-server
+ {
+ element gr:scheduled-tasks
+ {
+ map:get($roll-back, "scheduled-tasks")
+ }
+ },
element gr:http-servers
{
map:get($roll-back, "http-servers")
@@ -464,6 +471,7 @@ declare function setup:do-setup($import-config as element(configuration)) as ite
setup:configure-databases($import-config),
setup:create-appservers($import-config),
setup:apply-appservers-settings($import-config),
+ setup:create-scheduled-tasks($import-config),
if ($restart-needed) then
"note: restart required"
else ()
@@ -478,6 +486,26 @@ declare function setup:do-setup($import-config as element(configuration)) as ite
declare function setup:do-wipe($import-config as element(configuration)) as item()*
{
+ (: remove scheduled tasks :)
+ let $admin-config := admin:get-configuration()
+ let $remove-tasks :=
+ let $tasks :=
+ for $task in $import-config/gr:task-server/gr:scheduled-tasks/gr:scheduled-task
+ let $existing := setup:get-scheduled-task($task)
+ return
+ $existing
+ return
+ if ($tasks) then
+ xdmp:set(
+ $admin-config,
+ admin:group-delete-scheduled-task($admin-config, $default-group, $tasks))
+ else ()
+ return
+ if (admin:save-configuration-without-restart($admin-config)) then
+ xdmp:set($restart-needed, fn:true())
+ else (),
+
+ (: remove appservers :)
let $admin-config := admin:get-configuration()
let $groupid := xdmp:group()
let $remove-appservers :=
@@ -552,21 +580,44 @@ declare function setup:do-wipe($import-config as element(configuration)) as item
)
for $db-config in $databases
return
- setup:delete-database-and-forests($db-config),
+ setup:delete-databases($db-config),
- (: Even though we delete forests that are attached to the database above, we will delete
- : forests named in the config file. When named forests are in use, we'll be able to
- : delete them even if they aren't attached to the database for whatever reason. :)
let $admin-config := admin:get-configuration()
let $remove-forests :=
- for $forest-name in $import-config/as:assignments/as:assignment/as:forest-name
+ let $all-replica-names as xs:string* := $import-config/as:assignments/as:assignment/as:replica-names/as:replica-name
+ for $assignment in $import-config/as:assignments/as:assignment[fn:not(as:forest-name = $all-replica-names)]
+ let $forest-name := $assignment/as:forest-name
+ let $replica-names := $assignment/as:replica-names/as:replica-name[fn:string-length(fn:string(.)) > 0]
return
if (admin:forest-exists($admin-config, $forest-name)) then
+ let $forest-id := admin:forest-get-id($admin-config, $forest-name)
+ return
+ (
+ for $replica-name in $replica-names
+ where admin:forest-exists($admin-config, $replica-name)
+ return
+ let $replica-id := admin:forest-get-id($admin-config, $replica-name)
+ return
+ (
+ xdmp:set($admin-config, admin:forest-remove-replica($admin-config, $forest-id, $replica-id)),
+ xdmp:set($admin-config, admin:forest-delete($admin-config, $replica-id, fn:true()))
+ ),
+
+ try {
xdmp:set(
$admin-config,
admin:forest-delete(
$admin-config,
- admin:forest-get-id($admin-config, $forest-name), fn:true()))
+ $forest-id, fn:true()))
+ }
+ catch($ex) {
+ xdmp:set(
+ $admin-config,
+ admin:forest-delete(
+ $admin-config,
+ $forest-id, fn:false()))
+ }
+ )
else ()
return
if (admin:save-configuration-without-restart($admin-config)) then
@@ -661,7 +712,7 @@ declare function setup:do-wipe($import-config as element(configuration)) as item
else ()
};
-declare function setup:delete-database-and-forests($db-config as element(db:database))
+declare function setup:delete-databases($db-config as element(db:database))
{
let $db-name := $db-config/db:database-name
let $admin-config := admin:get-configuration()
@@ -670,34 +721,14 @@ declare function setup:delete-database-and-forests($db-config as element(db:data
let $db-id := admin:database-get-id($admin-config, $db-name)
let $forest-ids := admin:database-get-attached-forests($admin-config, $db-id)
let $detach :=
- (
for $id in $forest-ids
return
- xdmp:set($admin-config, admin:database-detach-forest($admin-config, $db-id, $id)),
- if (admin:save-configuration-without-restart($admin-config)) then
- xdmp:set($restart-needed, fn:true())
- else ()
- )
- let $admin-config := admin:get-configuration()
- let $forest-ids :=
- if (fn:exists($forest-ids)) then
- $forest-ids
- else
- (: For the case where the database exists but the forests are detached :)
- setup:find-forest-ids($db-config)
- let $admin-config := admin:forest-delete($admin-config, $forest-ids, fn:true())
- let $admin-config := admin:database-delete($admin-config, $db-id)
+ xdmp:set($admin-config, admin:database-detach-forest($admin-config, $db-id, $id))
+ let $delete := xdmp:set($admin-config, admin:database-delete($admin-config, $db-id))
return
if (admin:save-configuration-without-restart($admin-config)) then
xdmp:set($restart-needed, fn:true())
else ()
- else
- (: The database does not exist. Check for the forests anyway :)
- let $forest-ids := setup:find-forest-ids($db-config)
- let $admin-config := admin:forest-delete($admin-config, $forest-ids, fn:true())
- return
- if (admin:save-configuration-without-restart($admin-config)) then
- xdmp:set($restart-needed, fn:true())
else ()
};
@@ -731,7 +762,7 @@ declare function setup:find-forest-ids(
$db-config as element(db:database)) as xs:unsignedLong*
{
let $admin-config := admin:get-configuration()
- for $host at $position in admin:group-get-host-ids($admin-config, xdmp:group())
+ for $host at $position in admin:group-get-host-ids($admin-config, xdmp:group())
for $j in (1 to $db-config/db:forests-per-host)
let $name :=
fn:string-join((
@@ -816,14 +847,17 @@ declare function setup:create-forests-from-config(
$database-name as xs:string) as item()*
{
for $forest-config in setup:get-database-forest-configs($import-config, $database-name)
- let $forest-name as xs:string? := $forest-config/as:forest-name[fn:string-length(fn:string(.)) > 0]
+ for $forest-name as xs:string in $forest-config/as:forest-name[fn:string-length(fn:string(.)) > 0]
let $data-directory as xs:string? := $forest-config/as:data-directory[fn:string-length(fn:string(.)) > 0]
- let $host-name as xs:string? := $forest-config/as:host[fn:string-length(fn:string(.)) > 0]
+ let $host-name as xs:string? := $forest-config/as:host-name[fn:string-length(fn:string(.)) > 0]
+ let $replica-names as xs:string* := $forest-config/as:replica-names/as:replica-name[fn:string-length(fn:string(.)) > 0]
+ let $replicas := $import-config/as:assignments/as:assignment[as:forest-name = $replica-names]
return
setup:create-forest(
$forest-name,
$data-directory,
- if ($host-name) then xdmp:host($host-name) else ())
+ if ($host-name) then xdmp:host($host-name) else (),
+ $replicas)
};
@@ -835,12 +869,14 @@ declare function setup:validate-forests-from-config(
for $forest-config in setup:get-database-forest-configs($import-config, $database-name)
let $forest-name as xs:string? := $forest-config/as:forest-name[fn:string-length(fn:string(.)) > 0]
let $data-directory as xs:string? := $forest-config/as:data-directory[fn:string-length(fn:string(.)) > 0]
- let $host-name as xs:string? := $forest-config/as:host[fn:string-length(fn:string(.)) > 0]
+ let $host-name as xs:string? := $forest-config/as:host-name[fn:string-length(fn:string(.)) > 0]
+ let $replica-names as xs:string* := $forest-config/as:replica-names/as:replica-name[fn:string-length(fn:string(.)) > 0]
return
setup:validate-forest(
$forest-name,
$data-directory,
- if ($host-name) then xdmp:host($host-name) else ())
+ if ($host-name) then xdmp:host($host-name) else (),
+ $replica-names)
};
declare function setup:create-forests-from-count(
@@ -849,14 +885,16 @@ declare function setup:create-forests-from-count(
$forests-per-host as xs:int) as item()*
{
let $data-directory := $db-config/db:forests/db:data-directory
- for $host at $position in admin:group-get-host-ids(admin:get-configuration(), xdmp:group())
+ for $host at $position in admin:group-get-host-ids(admin:get-configuration(), xdmp:group())
for $j in (1 to $forests-per-host)
let $forest-name := fn:string-join(($database-name, fn:format-number(xs:integer($position), "000"), xs:string($j)), "-")
+ let $replica-names as xs:string* := ()
return
setup:create-forest(
$forest-name,
$data-directory,
- $host)
+ $host,
+ $replica-names)
};
declare function setup:validate-forests-from-count(
@@ -865,14 +903,16 @@ declare function setup:validate-forests-from-count(
$forests-per-host as xs:int)
{
let $data-directory := $db-config/db:forests/db:data-directory
- for $host at $position in admin:group-get-host-ids(admin:get-configuration(), xdmp:group())
+ for $host at $position in admin:group-get-host-ids(admin:get-configuration(), xdmp:group())
for $j in (1 to $forests-per-host)
let $forest-name := fn:string-join(($database-name, fn:format-number(xs:integer($position), "000"), xs:string($j)), "-")
+ let $replicas as xs:string* := ()
return
setup:validate-forest(
$forest-name,
$data-directory,
- $host)
+ $host,
+ $replicas)
};
declare function setup:get-database-forest-configs(
@@ -886,14 +926,34 @@ declare function setup:get-database-forest-configs(
declare function setup:create-forest(
$forest-name as xs:string,
$data-directory as xs:string?,
- $host-id as xs:unsignedLong?) as item()*
+ $host-id as xs:unsignedLong?,
+ $replicas as element(as:assignment)*) as item()*
{
if (xdmp:forests()[$forest-name = xdmp:forest-name(.)]) then
fn:concat("Forest ", $forest-name, " already exists, not recreated..")
else
+ (
let $host := ($host-id, $default-host)[1]
let $admin-config :=
admin:forest-create(admin:get-configuration(), $forest-name, $host, $data-directory)
+ let $forest-id := admin:forest-get-id($admin-config, $forest-name)
+ let $_ :=
+ for $replica in $replicas
+ let $replica-host-name := $replica/as:host-name[fn:string-length(fn:string(.)) > 0]
+ let $replica-host-id :=
+ if ($replica-host-name) then xdmp:host($replica-host-name) else ()
+ let $replica-host := ($replica-host-id, $default-host)[1]
+ let $cfg :=
+ admin:forest-create(
+ $admin-config,
+ $replica/as:forest-name,
+ $replica-host,
+ $replica/as:data-directory[fn:string-length(fn:string(.)) > 0])
+ let $replica-id := admin:forest-get-id($cfg, $replica/as:forest-name)
+ let $cfg := admin:forest-set-failover-enable($cfg, $forest-id, fn:true())
+ let $cfg := admin:forest-set-failover-enable($cfg, $replica-id, fn:true())
+ return
+ xdmp:set($admin-config, admin:forest-add-replica($cfg, $forest-id, $replica-id))
return
(
if (admin:save-configuration-without-restart($admin-config)) then
@@ -912,12 +972,14 @@ declare function setup:create-forest(
if ($host) then (" on ", xdmp:host-name($host))
else ()), "")
)
+ )
};
declare function setup:validate-forest(
$forest-name as xs:string,
$data-directory as xs:string?,
- $host-id as xs:unsignedLong?)
+ $host-id as xs:unsignedLong?,
+ $replica-names as xs:string*)
{
if (xdmp:forests()[$forest-name = xdmp:forest-name(.)]) then
let $forest-id := xdmp:forest($forest-name)
@@ -938,6 +1000,18 @@ declare function setup:validate-forest(
if ($actual = $host-id) then ()
else
setup:validation-fail(fn:concat("Forest host mismatch: ", $host-id, " != ", $actual))
+ else (),
+
+ if ($replica-names) then
+ let $actual := admin:forest-get-replicas($admin-config, $forest-id)
+ let $expected :=
+ for $replica-name in $replica-names
+ return
+ admin:forest-get-id($admin-config, $replica-name)
+ return
+ if ($actual = $expected) then ()
+ else
+ setup:validation-fail(fn:concat("Forest replica mismatch: ", fn:string-join($expected, ", "), " != ", fn:string-join($actual, ", ")))
else ()
)
else
@@ -1025,7 +1099,7 @@ declare function setup:validate-attached-forests-by-config(
declare function setup:attach-forests-by-count($db-config as element(db:database)) as item()*
{
let $database-name := setup:get-database-name-from-database-config($db-config)
- for $host at $position in admin:group-get-host-ids(admin:get-configuration(), xdmp:group())
+ for $host at $position in admin:group-get-host-ids(admin:get-configuration(), xdmp:group())
let $hostname := xdmp:host-name($host)
for $j in (1 to setup:get-forests-per-host-from-database-config($db-config))
let $forest-name := fn:string-join(($database-name, fn:format-number(xs:integer($position), "000"), xs:string($j)), "-")
@@ -1036,7 +1110,7 @@ declare function setup:attach-forests-by-count($db-config as element(db:database
declare function setup:validate-attached-forests-by-count($db-config as element(db:database))
{
let $database-name := setup:get-database-name-from-database-config($db-config)
- for $host at $position in admin:group-get-host-ids(admin:get-configuration(), xdmp:group())
+ for $host at $position in admin:group-get-host-ids(admin:get-configuration(), xdmp:group())
let $hostname := xdmp:host-name($host)
for $j in (1 to setup:get-forests-per-host-from-database-config($db-config))
let $forest-name := fn:string-join(($database-name, fn:format-number(xs:integer($position), "000"), xs:string($j)), "-")
@@ -1109,7 +1183,11 @@ declare function setup:validate-database-settings($import-config as element(conf
for $db-config in setup:get-databases-from-config($import-config)
let $database := xdmp:database(setup:get-database-name-from-database-config($db-config))
for $setting in $database-settings/*:setting
- let $expected := fn:data(xdmp:value(fn:concat("$db-config/db:", $setting)))
+ let $min-version as xs:string? := $setting/@min-version
+ let $expected :=
+ if (fn:empty($min-version) or setup:at-least-version($min-version)) then
+ fn:data(xdmp:value(fn:concat("$db-config/db:", $setting)))
+ else ()
let $actual :=
try
{
@@ -3217,7 +3295,11 @@ declare function setup:validate-server(
else
setup:validation-fail(fn:concat("Appserver last-login mismatch: ", $expected, " != ", $actual))
for $setting in $http-server-settings/*:setting
- let $expected := fn:data(xdmp:value(fn:concat("$server-config/gr:", $setting, "[fn:string-length(fn:string(.)) > 0]")))
+ let $min-version as xs:string? := $setting/@min-version
+ let $expected :=
+ if (fn:empty($min-version) or setup:at-least-version($min-version)) then
+ fn:data(xdmp:value(fn:concat("$server-config/gr:", $setting, "[fn:string-length(fn:string(.)) > 0]")))
+ else ()
let $actual := xdmp:value(fn:concat("admin:appserver-get-", $setting, "($admin-config, $server-id)"))
where $expected
return
@@ -3234,6 +3316,151 @@ declare function setup:validate-server(
setup:validation-fail(fn:concat("Appserver missing namespace: ", $expected/gr:namespace-uri))
};
+declare function setup:create-scheduled-tasks(
+ $import-config as element(configuration))
+{
+ let $tasks :=
+ for $task in $import-config/gr:task-server/gr:scheduled-tasks/gr:scheduled-task
+ let $existing := setup:get-scheduled-task($task)
+ where fn:not(fn:exists($existing))
+ return
+ setup:create-scheduled-task($task)
+ let $admin-config :=
+ admin:group-add-scheduled-task(
+ admin:get-configuration(),
+ $default-group,
+ $tasks)
+ return
+ (
+ if (admin:save-configuration-without-restart($admin-config)) then
+ xdmp:set($restart-needed, fn:true())
+ else (),
+ if ($tasks) then
+ setup:add-rollback("scheduled-tasks", $tasks)
+ else (),
+ fn:concat("Scheduled tasks created succesfully.")
+ )
+};
+
+declare function setup:create-scheduled-task(
+ $task as element(gr:scheduled-task))
+{
+ let $admin-config := admin:get-configuration()
+ return
+ if ($task/gr:task-type eq "daily") then
+ admin:group-daily-scheduled-task(
+ $task/gr:task-path,
+ $task/gr:task-root,
+ $task/gr:task-period,
+ $task/gr:task-start-time,
+ admin:database-get-id($admin-config, $task/gr:task-database/@name),
+ admin:database-get-id($admin-config, $task/gr:task-modules/@name),
+ xdmp:user($task/gr:task-user/@name),
+ $task/gr:task-host/xdmp:host(.),
+ $task/gr:task-priority)
+ else if ($task/gr:task-type eq "hourly") then
+ admin:group-hourly-scheduled-task(
+ $task/gr:task-path,
+ $task/gr:task-root,
+ $task/gr:task-period,
+ $task/gr:task-minute,
+ admin:database-get-id($admin-config, $task/gr:task-database/@name),
+ admin:database-get-id($admin-config, $task/gr:task-modules/@name),
+ xdmp:user($task/gr:task-user/@name),
+ $task/gr:task-host/xdmp:host(.),
+ $task/gr:task-priority)
+ else if ($task/gr:task-type eq "minutely") then
+ admin:group-minutely-scheduled-task(
+ $task/gr:task-path,
+ $task/gr:task-root,
+ $task/gr:task-period,
+ admin:database-get-id($admin-config, $task/gr:task-database/@name),
+ admin:database-get-id($admin-config, $task/gr:task-modules/@name),
+ xdmp:user($task/gr:task-user/@name),
+ $task/gr:task-host/xdmp:host(.),
+ $task/gr:task-priority)
+ else if ($task/gr:task-type eq "monthly") then
+ admin:group-monthly-scheduled-task(
+ $task/gr:task-path,
+ $task/gr:task-root,
+ $task/gr:task-period,
+ $task/gr:task-month-day,
+ $task/gr:task-start-time,
+ admin:database-get-id($admin-config, $task/gr:task-database/@name),
+ admin:database-get-id($admin-config, $task/gr:task-modules/@name),
+ xdmp:user($task/gr:task-user/@name),
+ $task/gr:task-host/xdmp:host(.),
+ $task/gr:task-priority)
+ else if ($task/gr:task-type eq "once") then
+ admin:group-one-time-scheduled-task(
+ $task/gr:task-path,
+ $task/gr:task-root,
+ $task/gr:task-start,
+ admin:database-get-id($admin-config, $task/gr:task-database/@name),
+ admin:database-get-id($admin-config, $task/gr:task-modules/@name),
+ xdmp:user($task/gr:task-user/@name),
+ $task/gr:task-host/xdmp:host(.),
+ $task/gr:task-priority)
+ else if ($task/gr:task-type eq "weekly") then
+ admin:group-weekly-scheduled-task(
+ $task/gr:task-path,
+ $task/gr:task-root,
+ $task/gr:task-period,
+ $task/gr:task-days/gr:task-day,
+ $task/gr:task-start-time,
+ admin:database-get-id($admin-config, $task/gr:task-database/@name),
+ admin:database-get-id($admin-config, $task/gr:task-modules/@name),
+ xdmp:user($task/gr:task-user/@name),
+ $task/gr:task-host/xdmp:host(.),
+ $task/gr:task-priority)
+ else ()
+};
+
+declare function setup:validate-scheduled-tasks(
+ $import-config as element(configuration))
+{
+ for $task in $import-config/gr:task-server/gr:scheduled-tasks/gr:scheduled-task
+ return
+ setup:validate-scheduled-task($task)
+};
+
+declare function setup:validate-scheduled-task(
+ $task as element(gr:scheduled-task))
+{
+ if (fn:not(fn:empty(setup:get-scheduled-task($task)))) then ()
+ else
+ setup:validation-fail(fn:concat("Validation fail for ", xdmp:describe($task)))
+};
+
+declare function setup:get-scheduled-task(
+ $task as element(gr:scheduled-task)) as element(gr:scheduled-task)?
+{
+ let $admin-config := admin:get-configuration()
+ let $tasks :=
+ admin:group-get-scheduled-tasks(
+ $admin-config,
+ $default-group)
+ let $tasks :=
+ $tasks[gr:task-path = $task/gr:task-path and
+ gr:task-root = $task/gr:task-root and
+ gr:task-type = $task/gr:task-type and
+ gr:task-database = admin:database-get-id($admin-config, $task/gr:task-database/@name) and
+ gr:task-modules = admin:database-get-id($admin-config, $task/gr:task-modules/@name) and
+ gr:task-user = xdmp:user($task/gr:task-user/@name)]
+ let $_ := xdmp:log(("task-period: ", $task/gr:task-period))
+ let $filtered := $tasks[if ($task/gr:task-period) then gr:task-period = $task/gr:task-period else fn:true()]
+ let $_ := xdmp:log(("filtered: ", $filtered))
+ return
+ $filtered
+ (:[if ($task/gr:task-period) then gr:task-period = $task/gr:task-period else fn:true()]:)
+(: [if ($task/gr:task-start-time) then gr:task-start-time = $task/gr:task-start-time else fn:true()]
+ [if ($task/gr:task-minute) then gr:task-minute = $task/gr:task-minute else fn:true()]
+ [if ($task/gr:task-month-day) then gr:task-month-day = $task/gr:task-month-day else fn:true()]
+ [if ($task/gr:task-days/gr:task-day) then fn:not(gr:task-days/gr:task-day != $task/gr:task-days/gr:task-day) else fn:true()]
+ [if ($task/gr:task-host) then gr:task-host = $task/gr:task-host/xdmp:host(.) else fn:true()]
+ [if ($task/gr:task-priority) then gr:task-priority = $task/gr:task-priority else fn:true()]:)
+};
+
declare function setup:create-privileges(
$import-config as element(configuration))
{
@@ -4351,7 +4578,8 @@ declare function setup:validate-install($import-config as element(configuration)
setup:validate-amps($import-config),
setup:validate-database-settings($import-config),
setup:validate-databases-indexes($import-config),
- setup:validate-appservers($import-config)
+ setup:validate-appservers($import-config),
+ setup:validate-scheduled-tasks($import-config)
}
catch($ex)
{
diff --git a/deploy/sample/ml-config.sample.xml b/deploy/sample/ml-config.sample.xml
index 8d589a6f..0031e800 100644
--- a/deploy/sample/ml-config.sample.xml
+++ b/deploy/sample/ml-config.sample.xml
@@ -13,7 +13,73 @@
true
true
true
-
+
+
+ /some/daily-task.xqy
+ /
+ daily
+ 2
+ 13:00:00-05:00
+
+
+
+
+
+ /some/hourly-task.xqy
+ /
+ hourly
+ 2
+ 15
+
+
+
+
+
+ /some/minutely-task.xqy
+ /
+ minutely
+ 3
+
+
+
+
+
+ /some/monthly-task.xqy
+ /
+ monthly
+ 1
+ 15
+ 13:00:00-05:00
+
+
+
+
+
+ /some/once-task.xqy
+ /
+ once
+ 1
+ 2019-01-01T13:00:00-05:00
+
+
+
+
+
+ /some/weekly-task.xqy
+ /
+ weekly
+ 1
+
+ monday
+ wednesday
+ friday
+
+ 13:00:00-05:00
+
+
+
+
+
-->
diff --git a/deploy/test/data/ml6-config.xml b/deploy/test/data/ml6-config.xml
index ae64a777..431f62cb 100644
--- a/deploy/test/data/ml6-config.xml
+++ b/deploy/test/data/ml6-config.xml
@@ -11,7 +11,72 @@
true
true
true
-
+
+
+ /some/daily-task.xqy
+ /
+ daily
+ 2
+ 13:00:00-05:00
+
+
+
+
+
+ /some/hourly-task.xqy
+ /
+ hourly
+ 2
+ 15
+
+
+
+
+
+ /some/minutely-task.xqy
+ /
+ minutely
+ 3
+
+
+
+
+
+ /some/monthly-task.xqy
+ /
+ monthly
+ 1
+ 15
+ 13:00:00-05:00
+
+
+
+
+
+ /some/once-task.xqy
+ /
+ once
+ 2019-01-01T13:00:00-05:00
+
+
+
+
+
+ /some/weekly-task.xqy
+ /
+ weekly
+ 1
+
+ monday
+ wednesday
+ friday
+
+ 13:00:00-05:00
+
+
+
+
+
diff --git a/deploy/test/data/ml7-config.xml b/deploy/test/data/ml7-config.xml
new file mode 100644
index 00000000..bcbeb1f0
--- /dev/null
+++ b/deploy/test/data/ml7-config.xml
@@ -0,0 +1,801 @@
+
+
+ 16
+ 16
+ 3600
+ 600
+ 100000
+ 1000
+ 1000
+ 10000
+ true
+ true
+ true
+
+
+ /some/daily-task.xqy
+ /
+ daily
+ 2
+ 13:00:00-05:00
+
+
+
+
+
+ /some/hourly-task.xqy
+ /
+ hourly
+ 2
+ 15
+
+
+
+
+
+ /some/minutely-task.xqy
+ /
+ minutely
+ 3
+
+
+
+
+
+ /some/monthly-task.xqy
+ /
+ monthly
+ 1
+ 15
+ 13:00:00-05:00
+
+
+
+
+
+ /some/once-task.xqy
+ /
+ once
+ 2019-01-01T13:00:00-05:00
+
+
+
+
+
+ /some/weekly-task.xqy
+ /
+ weekly
+ 1
+
+ monday
+ wednesday
+ friday
+
+ 13:00:00-05:00
+
+
+
+
+
+
+
+
+
+ @ml.app-name
+ @ml.app-port
+
+
+ 0
+ false
+ 0.0.0.0
+ 256
+ 32
+ 30
+ 5
+ 3600
+ 3600
+ 600
+ 3600
+ 1000
+ 10000
+ http://marklogic.com/collation/
+ digest
+ @ml.default-user
+ admin-ui
+ 0
+ false
+ true
+ true
+ true
+ 1.0-ml
+ contemporaneous
+ none
+ UTF-8
+ default
+ default
+
+
+
+
+ default
+ default
+ default
+ default
+
+ none
+ default
+ omit
+ default
+
+ default
+ /roxy/error.xqy
+
+
+
+ ns1
+ http://www.ns.com/ns1
+
+
+ ns2
+ http://www.ns.com/ns2
+
+
+
+ /roxy/rewrite.xqy
+ true
+
+ true
+ true
+ sixer
+ ALL:!LOW:@STRENGTH
+ true
+
+ @ml.test-appserver
+
+
+ @ml.xdbc-server
+
+
+
+
+ @ml.content-db
+
+ @ml.test-content-db-assignment
+ @ml.test-modules-db-assignment
+
+ @ml.modules-db
+
+ @ml.schemas-assignment
+ @ml.triggers-assignment
+
+
+
+
+ @ml.test-content-db-xml
+
+ @ml.content-db
+ @ml.content-forests-per-host
+ @ml.schemas-mapping
+ @ml.triggers-mapping
+
+
+
+ true
+ @ml.content-db-security
+ en
+
+
+ http://www.marklogic.com/ns/sample
+ frag-root
+
+
+ http://www.marklogic.com/ns/sample2
+ frag-root2
+
+
+
+
+ http://www.marklogic.com/ns/sample
+ frag-parents
+
+
+ http://www.marklogic.com/ns/sample
+ frag-parents2
+
+
+
+
+ http://schemas.microsoft.com/office/word/2003/wordml
+ p
+
+
+ http://schemas.openxmlformats.org/wordprocessingml/2006/main
+ p
+
+
+
+
+ http://www.w3.org/1999/xhtml
+ a abbr acronym b big br center cite code dfn em font i kbd q samp small span strong sub sup tt var
+
+
+ http://schemas.microsoft.com/office/word/2003/wordml
+ br cr fldChar fldData fldSimple hlink noBreakHyphen permEnd permStart pgNum proofErr r softHyphen sym t tab
+
+
+ http://schemas.microsoft.com/office/word/2003/auxHint
+ t
+
+
+ http://schemas.openxmlformats.org/wordprocessingml/2006/main
+ r t endnoteReference footnoteReference customXml hyperlink sdt sdtContent commentRangeEnd commentRangeStart bookmarkStart bookmarkEnd fldSimple instrText smartTag ins proofErr
+
+
+ http://marklogic.com/entity
+ person organization location gpe facility religion nationality credit-card-number email coordinate money percent id phone-number url utm date time
+
+
+
+
+ http://schemas.microsoft.com/office/word/2003/wordml
+ delInstrText delText endnote footnote instrText pict rPr
+
+
+ http://schemas.openxmlformats.org/wordprocessingml/2006/main
+ pPr rPr customXmlPr sdtPr commentReference del
+
+
+
+
+
+ false
+ decompounding
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+
+ http://www.marklogic.com/ns/sample
+ word-query-include
+ 1.0
+
+
+
+
+
+ http://www.marklogic.com/ns/sample2
+ word-query-include2
+ 1.0
+
+
+
+
+
+
+
+ http://www.marklogic.com/ns/sample
+ word-query-exclude
+
+
+
+
+
+ http://www.marklogic.com/ns/sample2
+ word-query-exclude2
+
+
+
+
+
+
+
+ test
+ true
+
+ http://marklogic.com/collation/
+ http://marklogic.com/collation/codepoint
+
+
+
+ http://www.marklogic.com/ns/sample
+ sample-included-element
+ 1
+
+
+
+
+
+ http://www.marklogic.com/ns/sample2
+ sample-included-element2
+ 1
+
+
+
+
+
+
+
+ http://www.marklogic.com/ns/sample
+ sample-excluded-element
+
+
+
+
+
+ http://www.marklogic.com/ns/sample2
+ sample-excluded-element2
+
+
+
+
+
+
+
+ test2
+ true
+
+
+
+ http://www.marklogic.com/ns/sample
+ sample-included-element
+ 1
+
+
+
+
+
+ http://www.marklogic.com/ns/sample2
+ sample-included-element2
+ 1
+
+
+
+
+
+
+
+ http://www.marklogic.com/ns/sample
+ sample-excluded-element
+
+
+
+
+
+ http://www.marklogic.com/ns/sample2
+ sample-excluded-element2
+
+
+
+
+
+
+
+
+
+ string
+ http://marklogic.com/ns/sample
+ name
+ http://marklogic.com/collation/codepoint
+ false
+ reject
+
+
+ string
+ http://marklogic.com/ns/sample2
+ name2
+ http://marklogic.com/collation/codepoint
+ false
+ reject
+
+
+
+
+ string
+ http://marklogic.com/ns/sample
+ person
+
+ name
+ http://marklogic.com/collation/codepoint
+ false
+ reject
+
+
+ string
+ http://marklogic.com/ns/sample2
+ person2
+
+ name2
+ http://marklogic.com/collation/codepoint
+ false
+ reject
+
+
+
+
+ date
+ sample
+
+ false
+ reject
+
+
+ date
+ sample2
+
+ false
+ reject
+
+
+
+
+ sample
+ http://marklogic.com/ns/sample
+
+
+ sample2
+ http://marklogic.com/ns/sample2
+
+
+
+
+ string
+ http://marklogic.com/collation/codepoint
+ /sample:root/sample:child
+ false
+ reject
+
+
+ string
+ http://marklogic.com/collation/codepoint
+ /sample:root/sample:child2
+ false
+ reject
+
+
+
+
+ http://www.marklogic.com/ns/sample
+ geo-element
+ wgs84
+ point
+ false
+ reject
+
+
+ http://www.marklogic.com/ns/sample
+ geo-element2
+ wgs84
+ point
+ false
+ reject
+
+
+
+
+ http://marklogic.com/ns/sample
+ geo
+
+ lat
+
+ lon
+ wgs84
+ false
+ reject
+
+
+ http://marklogic.com/ns/sample
+ geo2
+
+ lat2
+
+ lon2
+ wgs84
+ false
+ reject
+
+
+
+
+ http://marklogic.com/ns/sample
+ geo
+ http://marklogic.com/ns/sample
+ lat
+ http://marklogic.com/ns/sample
+ lon
+ wgs84
+ true
+ reject
+
+
+ http://marklogic.com/ns/sample
+ geo2
+ http://marklogic.com/ns/sample
+ lat2
+ http://marklogic.com/ns/sample
+ lon2
+ wgs84
+ true
+ reject
+
+
+
+
+ http://marklogic.com/ns/sample
+ geo
+ http://marklogic.com/ns/sample
+ pos
+ wgs84
+ point
+ false
+ reject
+
+
+ http://marklogic.com/ns/sample
+ geo2
+ http://marklogic.com/ns/sample
+ pos2
+ wgs84
+ point
+ false
+ reject
+
+
+
+
+ http://www.marklogic.com/ns/sample
+ sample-element
+ http://marklogic.com/collation/
+
+
+ http://www.marklogic.com/ns/sample
+ sample-element2
+ http://marklogic.com/collation/
+
+
+
+
+ http://www.marklogic.com/ns/sample
+ sample-element
+
+ sample-attribute
+ http://marklogic.com/collation/
+
+
+ http://www.marklogic.com/ns/sample2
+ sample-element2
+
+ sample-attribute2
+ http://marklogic.com/collation/
+
+
+
+
+
+ @ml.modules-db
+
+
+
+ basic
+ false
+ false
+ true
+ false
+ true
+ true
+ true
+ false
+ true
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+
+ http://marklogic.com/collation/
+ http://marklogic.com/collation/codepoint
+
+ false
+ false
+ true
+ false
+ true
+ 5
+ 0
+ manual
+ false
+ false
+ false
+ false
+ false
+ 32768
+ 64
+ 16
+ 2
+ 2
+ 1024
+ fast
+ fast
+ 128
+ false
+ false
+ false
+ facet-time
+ 256
+ automatic
+ automatic
+ automatic
+ scaled-log
+ lower
+ 0
+ 1024
+ 2
+ 0
+
+
+ @ml.test-modules-db-xml
+
+ @ml.triggers-db-xml
+ @ml.schemas-db-xml
+
+
+
+ @ml.app-role
+ A role for users of the @ml.app-name application
+
+
+
+
+ execute
+ @ml.app-role
+
+
+ update
+ @ml.app-role
+
+
+ insert
+ @ml.app-role
+
+
+ read
+ @ml.app-role
+
+
+
+
+
+
+ xdmp:value
+
+
+ xdmp:add-response-header
+
+
+ xdmp:invoke
+
+
+ xdmp:with-namespaces
+
+
+
+
+ @ml.app-role2
+ A second role for users of the @ml.app-name application
+
+
+
+
+ execute
+ @ml.app-role2
+
+
+ update
+ @ml.app-role2
+
+
+ insert
+ @ml.app-role2
+
+
+ read
+ @ml.app-role2
+
+
+
+
+
+
+ xdmp:value
+
+
+ xdmp:add-response-header
+
+
+ xdmp:invoke
+
+
+ xdmp:with-namespaces
+
+
+
+
+
+
+ @ml.app-name-user
+ A user for the @ml.app-name application
+ password
+
+ @ml.app-role
+
+
+
+
+
+ @ml.app-name-user2
+ A second user for the @ml.app-name application
+ password2
+
+ @ml.app-role2
+
+
+
+
+
+
+
+ http://marklogic.com/roxy
+ sample
+ /app/models/sample.xqy
+ @ml.app-modules-db
+ admin
+
+
+ http://marklogic.com/roxy
+ sample2
+ /app/models/sample2.xqy
+ @ml.app-modules-db
+ admin
+
+
+
+
+ my-action
+ http://marklogic.com/custom/privilege/my-action
+ execute
+
+
+ my-action2
+ http://marklogic.com/custom/privilege/my-action2
+ execute
+
+
+
+
+ application/crazy
+ crazy stuff
+ text
+
+
+ application/crazy2
+ crazy stuff 2
+ text
+
+
+
\ No newline at end of file
diff --git a/deploy/test/data/ml7-properties/build.properties b/deploy/test/data/ml7-properties/build.properties
new file mode 100644
index 00000000..86a9eddc
--- /dev/null
+++ b/deploy/test/data/ml7-properties/build.properties
@@ -0,0 +1,146 @@
+#################################################################
+# This file contains overrides to values in default.properties
+# Make changes here, not in default.properties
+#################################################################
+
+#
+# Admin username/password that will exist on the dev/cert/prod servers
+#
+user=admin
+password=admin
+
+# Your Application's name
+app-name=roxy-deployer-tester
+
+# The root of you modules database or filesystem dir
+modules-root=/
+
+#
+# the location of your marklogic configuration file
+#
+config.file=${basedir}/deploy/test/data/ml7-config.xml
+
+#
+# Unit Testing
+# Leave commented out for no unit testing
+# turn these on if you are using the roxy unit tester
+# Note: to activate Unit Testing, you must have test-content-db and test-port defined
+#
+# test-content-db=${app-name}-content-test
+# test-modules-db=${app-name}-modules
+# test-port=8042
+#
+# The authentication method used for your test appserver
+# application-level, basic, digest, digestbasic
+#
+# test-authentication-method=digest
+#
+# end of Unit Testing section
+
+#
+# Leave commented out for default
+#
+schemas-db=${app-name}-schemas
+
+#
+# Leave commented out for default
+# turn it on if you are using triggers or CPF
+#
+#triggers-db=${app-name}-triggers
+
+#
+# the port that the Docs appserver is running on
+# Docs appserver is required for boostrapping
+# set this to 8000 for ML 4.x and 8002 for ML 5.x
+# you should only override this if your Docs appserver
+# is running on a funky port
+#
+# bootstrap-port=
+
+#
+# The ports used by your application
+#
+app-port=8900
+xcc-port=8901
+# odbc-port=8043
+
+content-forests-per-host=2
+
+## Security
+#
+# The authentication method used for your appserver
+# application-level, basic, digest, digestbasic
+#
+authentication-method=digest
+#
+# The user used as the default user in application level authentication.
+# Using the admin user as the default user is equivalent to turning security off.
+#
+# default-user=${app-name}-user
+#
+# The password assigned to the default user for your application
+#
+appuser-password=Z:S1P%c9%afEqJUZu)UA
+#
+# To make your http appserver use TLS/SSL, uncomment the ssl-certificate-template
+# here and uncomment the ssl-certificate-template element in ml-config.xml.
+#
+# To create a template, uncomment at least ssl-certificate-template and ssl-certificate-oranizationName
+# ssl-certificate-template=roxy
+# ssl-certificate-countryName=US
+# ssl-certificate-stateOrProvinceName=LA
+# ssl-certificate-localityName=New Orleans
+# ssl-certificate-organizationName=Zulu Krewe
+# ssl-certificate-organizationalUnitName=BeadBase
+# ssl-certificate-emailAddress=changeme@example.com
+#
+# end of Security section
+
+#
+# The type of application. Choices are:
+# mvc: a normal, XQuery-based Roxy MVC app
+# rest: an app based on the ML6 REST API
+# hybrid: an app that uses Roxy rewriting and the ML6 REST API
+#
+app-type=
+
+#
+# The major version of MarkLogic server you are using
+# 4, 5, 6, 7
+#
+server-version=7
+
+#
+# the location of your REST API options
+# relevant to app-types rest and hybrid.
+#
+rest-options.dir=${basedir}/rest-api/config
+
+#
+# the location of your REST API extension modules
+# relevant to app-types rest and hybrid.
+#
+rest-ext.dir=${basedir}/rest-api/ext
+
+#
+# the location of your REST API transform modules
+# relevant to app-types rest and hybrid.
+#
+rest-transforms.dir=${basedir}/rest-api/transforms
+
+#
+# The Roxy rewriter handles both Roxy MVC and the ML REST API
+#
+url-rewriter=/roxy/rewrite.xqy
+error-handler=/roxy/error.xqy
+rewrite-resolves-globally=
+
+#
+# the uris or IP addresses of your servers
+# WARNING: if you are running these scripts on WINDOWS you may need to change localhost to 127.0.0.1
+# There have been reported issues with dns resolution when localhost wasn't in the hosts file.
+#
+local-server=localhost
+#dev-server=
+#cert-server=
+#prod-server=
diff --git a/deploy/test/data/ml7-properties/default.properties b/deploy/test/data/ml7-properties/default.properties
new file mode 100644
index 00000000..e3157d0a
--- /dev/null
+++ b/deploy/test/data/ml7-properties/default.properties
@@ -0,0 +1,181 @@
+#################################################################
+#
+# DO NOT EDIT
+#
+# This file contains default application configuration options.
+# Don't mess with this file. Instead, copy what you need to
+# build.properties and edit there.
+# This process makes upgrading Roxy easier.
+#################################################################
+
+#
+# the location of your code to load into ML
+#
+xquery.dir=${basedir}/src
+
+# MarkLogic application servers exist inside a group. ML Instances start off
+# with a group called "Default".
+group=Default
+#
+# The type of application. Choices are:
+# mvc: a normal, XQuery-based Roxy MVC app
+# rest: an app based on the ML6 REST API
+# hybrid: an app that uses Roxy rewriting and the ML6 REST API
+#
+app-type=mvc
+
+#
+# the location of your REST API options
+# relevant to app-types rest and hybrid.
+#
+rest-options.dir=${basedir}/rest-api/config
+
+#
+xquery-test.dir=${basedir}/src/test
+
+#
+# the location of your xml data to load into ML
+#
+data.dir=${basedir}/data
+
+#
+# the location of your marklogic configuration file
+#
+# the location of your schemas
+schemas.dir=${basedir}/schemas
+schemas.root=/
+
+#
+# Admin username/password that will exist on the local/dev/prod servers
+#
+
+#
+# Admin username/password that will exist on the dev/cert/prod servers
+#
+user=admin
+password=admin
+
+#
+# Your Application's name
+#
+app-name=roxy
+modules-root=/
+
+# The role that is given permissions and execute privileges
+app-role=${app-name}-role
+
+#
+# The names of your databases. Forests are given the same names
+#
+app-modules-db=${app-name}-modules
+content-db=${app-name}-content
+modules-db=${app-name}-modules
+
+#
+# Number of forests to create per host in the group for the content-db
+#
+content-forests-per-host=1
+
+#
+## Unit Testing
+# A location on disk to store the forest data. "data directory" in the admin ui
+#
+# Note: to activate Unit Testing, you must have test-content-db and test-port defined
+# forest-data-dir=
+
+#
+# The authentication method used for your test appserver
+# application-level, basic, digest, digestbasic
+#
+# test-authentication-method=digest
+#
+# Leave commented out for no test appserver
+# turn it on if you are using the roxy unit tester
+# test-port=8042
+#
+# the environments in which we DO NOT want to deploy tests
+# typically your production environment.
+do-not-deploy-tests=prod
+#
+# the location of your unit test code
+xquery-test.dir=${basedir}/src/test
+# Leave commented out for no test db
+
+# turn it on if you are using the roxy unit tester
+# test-content-db=${app-name}-content-test
+# test-modules-db=${app-modules-db}
+
+# Leave commented out for default
+# schemas-db=${app-name}-schemas
+
+# Leave commented out for default
+# turn it on if you are using triggers or CPF
+# triggers-db=${app-name}-triggers
+
+#
+# the port that the Docs appserver is running on
+# Docs appserver is required for boostrapping
+qconsole-port=8000
+
+#
+# If true, .html files under the src directory will be loaded as XML.
+# If false, they will be loaded using the MarkLogic default behavior, which is
+# to treat them as text.
+#
+load-html-as-xml=true
+
+#
+# If true, .js and .css files under the src directory will be loaded as binary.
+# If false, they will be loaded using the MarkLogic default behavior, which is
+# to treat them as text.
+#
+load-js-as-binary=true
+load-css-as-binary=true
+
+#
+bootstrap-port-five=8002
+bootstrap-port-four=8000
+
+#
+# The ports used by your application
+## Security
+# Leave commented out for no test appserver
+# turn it on if you are using the roxy unit tester
+# test-port=8042
+
+#
+# Providing a default empty value
+ssl-certificate-template=
+#
+# The authentication used for your appserver
+#
+authentication-method=digest
+default-user=${app-name}-user
+
+rewrite-resolves-globally=
+#
+# Environments recognized by Roxy
+# The default values point to Roxy file
+#
+url-rewriter=/roxy/rewrite.xqy
+# The Major version of ML server across your environments (4, 5, 6 or 7). You can override
+
+environments=local,dev,prod
+
+#
+# The Major version of ML server across your environments (4 or 5). You can override
+# this value in build.properties if all of your servers are the same version
+# or override it in each ${env}.properties file if each server has a different
+# version.
+#
+server-version=5
+
+#
+# the uris or IP addresses of your servers
+# WARNING: if you are running these scripts on windows you may need to change localhost to 127.0.0.1
+# There have been reported issues with dns resolution when localhost wasn't in the hosts file.
+#
+local-server=localhost
+#dev-server=
+#cert-server=
+#prod-server=