[MarkLogic Dev General] Scaffolding
Eric Palmitesta
eric.palmitesta at utoronto.ca
Wed Sep 24 14:55:38 PDT 2008
Hi all,
Our website translations reside in xml files, however it became rather
tedious to edit and re-load a local file xml file into MarkLogic for
every small change.
I've put together an xml scaffolding written in xquery. If you've never
heard of a software scaffolding before, it simply allows temporary
front-end access to back-end data, while the real front-end is still
under development. There are many sql-based scaffolding apps made for
php and other web-based languages.
The basic idea here is: provide a simple interface to quickly browse and
edit xml documents with a web browser.
WARNING: Scaffolding is still very much under construction, in fact
there are still data-destroying bugs present! Don't use Scaffolding on
anything other than throwaway/test xml documents!
To try it out, grab the attached scaffolding.xqy file, drop it into an
http app server, and point your browser to its location.
There is a bug I'm still stumped on, and would appreciate some help
with. When saving a document containing attributes which are part of an
element which has only text-node children (ex. <sign width="10"
height="5">hello</sign>), those attributes vanish (ex.
<sign>hello</sign>). However, if you grab the command from the log
(line 253) and execute it standalone (in CQ, for example), given the same
document, the attributes are saved successfully. Attributes which are
part of an element which has only element-node children are also saved
successfully.
If you do attempt to browse through the source, I'll apologize now about
the lack of comments and general messiness, it was put together in half
a day last week. To an xquery vet, however, it should be at least
somewhat intuitive.
I'd love to get ANY kind of feedback you've got.
Thanks,
Eric
-------------- next part --------------
xquery version "1.0"
(:
The main html page which wraps all other pages (display-info, display-dir, display-file)
:)
define function display-page($title as xs:string?, $content as item())
{
let $content-type := xdmp:set-response-content-type("text/html")
let $doc-type := '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
return
($doc-type,
<html>
<head>
<title>{ if ($title) then concat($title, ' - ') else () }Scaffolding</title>
<style type="text/css">
<!--
body { font-family: 'courier new'; font-size: 14px; }
div.element-contents ul li { list-style: none; }
.sub { font-size: 12px; }
-->
</style>
</head>
<body>
<h2><a href="{ tokenize(xdmp:get-request-path(), "/")[last()] }">Scaffolding</a></h2>
{ $content }
</body>
</html>)
}
define function display-info()
{
display-page((),
<div>
<form action="?" method="get">
Absolute uri (file or directory):<br />
<input type="text" name="uri" value="" size="40" maxlength="128" />
<input type="submit" value="View / Edit" />
</form>
<p>
Or manually edit the query string:<br />
<b>{ tokenize(xdmp:get-request-path(), "/")[last()] }?uri=/path/to/dir/</b> to list a directory<br />
<b>{ tokenize(xdmp:get-request-path(), "/")[last()] }?uri=/path/to/file.xml</b> to view/edit a file
</p>
<p><b>Abstract:</b> Scaffolding is useful for making quick text edits and minor structural
changes to existing xml files.
</p>
<p>
<b>Warning:</b> This script can make modifications to the contents of your MarkLogic
database. <b>USE AT YOUR OWN RISK</b>. Do not make this file accessible in a production
environment unless you really know what you're doing. <b>There are still unresolved bugs</b>,
see below.
</p>
<p><b>Notes:</b></p>
<ul>
<li>Able to make text-node updates to any xml documents it has access to.</li>
<li>Able to duplicate (insert) sibling elements, and remove elements altogether. Careful
when removing, if you remove the last node of it's kind, there's no way to bring it back.</li>
<li><b>Doesn't handle elements which contain both text-node AND element-node children</b>.
For example, an element containing markup-like content such as
{ xdmp:quote(<message>xquery is <b>lots</b> of <i>fun</i></message>) } will be read as
{ xdmp:quote(<message>xquery is lots of fun</message>) }.</li>
<li>The MarkLogic user executing this script must have permission to execute xdmp:eval.</li>
<li>This file is entirely self-contained, no dependancies. Just drop it anywhere in your
project and go.</li>
<li>Apologies for the complete lack of commenting, this is still in the works.</li>
</ul>
<p>
<b>Bugs:</b> When saving a document containing attributes which are part of an element which
has only text-node children (ex. { xdmp:quote(<sign width="10" height="5">hello</sign>) }),
those attributes vanish (ex. { xdmp:quote(<sign>hello</sign>) }).
Attributes which are part of an element which has only element-node children
are saved successfully. If you grab the command from the log and execute it standalone (in
CQ, for example), given the same document, the attributes are saved successfully.
I still have no idea why this is happening, see line 41 and 43 of this file.
</p>
<p>
<b>Motivation:</b> Our website translations reside in xml files. I find it useful to view a
translation with Scaffolding to make quick edits and additions to the output phrases, rather than
editing a local file and re-loading it into MarkLogic for every change.
</p>
<p>
<b>Contact:</b> Please direct all comments/suggestions to <b>eric.palmitesta at utoronto.ca</b>,
all feedback is welcome. I'd also love to know what you're using this utility for, and if you
found it useful.
</p>
<p>Eric Palmitesta<br />eric.palmitesta at utoronto.ca</p>
</div>
)
}
define function display-dir($uri as xs:string)
{
display-page($uri,
let $uri-with-slash := if (ends-with($uri, '/')) then $uri else concat($uri, '/')
let $files := for $file in xdmp:directory($uri-with-slash, 'infinity') return base-uri($file)
return
if (not(exists($files))) then
<p>resource empty or not found: <b>{ $uri }</b></p>
else
<div>
<p><b>{ display-path($uri-with-slash) }</b></p>
<ul>{
for $file in $files
order by $file
return
<li><a href="?uri={ $file }">{ $file }</a></li>
}</ul>
</div>
)
}
define function display-file($uri as xs:string)
{
display-page($uri,
if (not(doc-available($uri))) then
<p>file not found: <b>{ $uri }</b></p>
else
<div>
<form action="?uri={ $uri }&save=true" method="post">
<p><input type="submit" value="Save" /> <b>{ display-path($uri) }</b></p>
<div class="element-contents">{ display-element-contents(doc($uri)/element()) }</div>
<p><input type="submit" value="Save" /></p>
</form>
</div>
)
}
(:
The meat and potatoes of Scaffolding. Recurively walks the xml tree,
displaying its elements and attributes in html with appropriate text boxes.
:)
define function display-element-contents($elements as element()*)
{
for $element in $elements
return
<ul>
<li>{
open-tag($element),
if (exists($element/text()) or not(exists($element/element()))) then
let $text := string($element)
return
if (string-length($text) gt 30) then
<textarea name="__element__{ xdmp:path($element) }" rows="3" cols="30">{ $text }</textarea>
else
<input type="text" name="__element__{ xdmp:path($element) }" value="{ $text }"
size="{ min((30, string-length($text))) }" />
else
display-element-contents($element/element()),
close-tag($element)
} {
if (not($element = root($element))) then add-remove-links($element)
else ()
}</li>
</ul>
}
define function add-remove-links($element as element())
{
<span class="sub">
(<a href="?uri={ base-uri($element) }&insert={ xdmp:path($element) }" title="insert">+</a>
/
<a href="?uri={ base-uri($element) }&remove={ xdmp:path($element) }" title="remove">-</a>)
</span>
}
define function open-tag($element as element())
as xs:string
{
if (not($element/@*)) then
concat('<', node-name($element), '> ')
else (
concat('<', node-name($element)),
for $attribute in $element/@*
let $text := string($attribute)
return (
concat(' ', node-name($attribute), '='),
<input type="text" name="__attribute__{ xdmp:path($attribute) }" value="{ $text }"
size="{ min((30, string-length($text)+5)) }" />
),
'> '
)
}
define function close-tag($element as element())
as xs:string
{
concat(' </', node-name($element), '>')
}
(:
Takes a string such as "/path/to/file.xml", and returns a
slash-delimited list of links to each token in the path:
/<a href="/path">path</a>/<a href="/path/to">to</a>/<a href="/path/to/file.xml">file.xml</a>
:)
define function display-path($path as xs:string)
as element(span)
{
<span>{
let $link := ''
for $token in tokenize($path, '/')[2 to last()]
return
(xdmp:set($link, concat($link, '/', $token)),
<span>/<a href="?uri={ $link }">{ $token }</a></span>)
}</span>
}
define function insert-duplicate-sibling($uri as xs:string, $insert-path as xs:string)
{
let $node := concat("(doc('", $uri, "')", $insert-path, ')[last()]')
return
xdmp:eval(concat("xdmp:node-insert-after(", $node, ", ", $node, ")"))
}
define function remove-element($uri as xs:string, $remove-path as xs:string)
{
let $node := concat("(doc('", $uri, "')", $remove-path, ')[last()]')
return
xdmp:eval(concat("xdmp:node-delete(", $node, ")"))
}
define function save-fields($uri as xs:string, $fields as xs:string*)
{
for $path in $fields
return
if (starts-with($path, '__element__')) then
update-element($uri, substring-after($path, '__element__'), xdmp:get-request-field($path))
else if (starts-with($path, '__attribute__')) then
update-attribute($uri, substring-after($path, '__attribute__'), xdmp:get-request-field($path))
else ()
}
(:
Called by save-fields, constructs a new element containing $value, to replace
the element residing at $path in $uri.
:)
define function update-element($uri as xs:string, $path as xs:string, $value as xs:string)
{
let $old-node := concat("doc('", $uri, "')", $path)
let $new-node := concat("element { '", tokenize(string-join(tokenize($path, "\[\d+\]"), ''), '/')[last()], "' } { '", replace($value, "'", "''"), "' }")
let $z := xdmp:log(concat("xdmp:node-replace(", $old-node, ", ", $new-node, ")"))
return
xdmp:eval(concat("xdmp:node-replace(", $old-node, ", ", $new-node, ")"))
}
(:
Called by save-fields, constructs a new attribute containing $value, to replace
the attribute residing at $path in $uri.
:)
define function update-attribute($uri as xs:string, $path as xs:string, $value as xs:string)
{
let $old-node := concat("doc('", $uri, "')", $path)
let $new-node := concat("attribute { '", substring-after($path, '@'), "' } { '", replace($value, "'", "''"), "' }")
let $z := xdmp:log(concat("xdmp:node-replace(", $old-node, ", ", $new-node, ")"))
return
xdmp:eval(concat("xdmp:node-replace(", $old-node, ", ", $new-node, ")"))
}
(: THE REAL WORK STARTS HERE :)
let $save := xdmp:get-request-field('save', '')
let $insert := xdmp:get-request-field('insert', '')
let $remove := xdmp:get-request-field('remove', '')
let $uri := xdmp:get-request-field('uri', '')
return
if ($save) then
(save-fields($uri, xdmp:get-request-field-names()),
xdmp:redirect-response(concat('?uri=', $uri)))
else if ($insert) then
(insert-duplicate-sibling($uri, $insert),
xdmp:redirect-response(concat('?uri=', $uri)))
else if ($remove) then
(remove-element($uri, $remove),
xdmp:redirect-response(concat('?uri=', $uri)))
else if (ends-with($uri, '.xml')) then
display-file($uri)
else if ($uri) then
display-dir($uri)
else
display-info()
More information about the General
mailing list