From 65fda43d6d0e8a561af428f0802804d8a8805c3b Mon Sep 17 00:00:00 2001 From: Mike McKay <mfmckay@wustl.edu> Date: Thu, 24 Mar 2016 16:19:54 -0500 Subject: [PATCH] XNAT-3791 Made changes to enable script versioning. --- .../org/nrg/xnat/restlet/XNATApplication.java | 3 +- .../restlet/resources/ScriptResource.java | 57 +++-- .../resources/ScriptVersionsResource.java | 144 ++++++++++++ .../webapp/scripts/xnat/app/scriptEditor.js | 208 +++++++++++------- .../xnat-templates/screens/EditScript.vm | 3 + .../webapp/xnat-templates/screens/Scripts.vm | 14 +- 6 files changed, 324 insertions(+), 105 deletions(-) create mode 100644 src/main/java/org/nrg/xnat/restlet/resources/ScriptVersionsResource.java diff --git a/src/main/java/org/nrg/xnat/restlet/XNATApplication.java b/src/main/java/org/nrg/xnat/restlet/XNATApplication.java index a78986a6..64cd2074 100755 --- a/src/main/java/org/nrg/xnat/restlet/XNATApplication.java +++ b/src/main/java/org/nrg/xnat/restlet/XNATApplication.java @@ -337,7 +337,8 @@ public class XNATApplication extends Application { attachURI(router, "/services/features", FeatureDefinitionRestlet.class); attachURIs(router, ScriptRunnerResource.class, "/automation/runners", "/automation/runners/{LANGUAGE}", "/automation/runners/{LANGUAGE}/{VERSION}"); - attachURIs(router, ScriptResource.class, "/automation/scripts", "/automation/scripts/{SCRIPT_ID}"); + attachURIs(router, ScriptResource.class, "/automation/scripts", "/automation/scripts/{SCRIPT_ID}", "/automation/scripts/{SCRIPT_ID}/{VERSION}"); + attachURIs(router, ScriptVersionsResource.class, "/automation/scriptVersions", "/automation/scriptVersions/{SCRIPT_ID}"); attachURIs(router, EventResource.class, "/automation/events", "/automation/events/{EVENT_ID}"); attachURIs(router, WorkflowEventResource.class, "/automation/workflows", "/automation/workflows/{SPEC}"); attachURIs(router, ScriptTriggerResource.class, "/automation/handlers", diff --git a/src/main/java/org/nrg/xnat/restlet/resources/ScriptResource.java b/src/main/java/org/nrg/xnat/restlet/resources/ScriptResource.java index bea77c2f..6407ba30 100644 --- a/src/main/java/org/nrg/xnat/restlet/resources/ScriptResource.java +++ b/src/main/java/org/nrg/xnat/restlet/resources/ScriptResource.java @@ -44,6 +44,8 @@ public class ScriptResource extends AutomationResource { _scriptId = (String) getRequest().getAttributes().get(SCRIPT_ID); + _version = (String) getRequest().getAttributes().get(VERSION); + // If the user isn't a site admin, there's a limited set of operations they are permitted to perform. if (!Roles.isSiteAdmin(user)) { // You can't put or post or delete a script and you can't retrieve a specific script OTHER THAN the split @@ -91,21 +93,27 @@ public class ScriptResource extends AutomationResource { if (StringUtils.isNotBlank(_scriptId)) { try { - // They're requesting a specific script, so return that to them. - Script script = getScript(); - - // Here's a special case: if they're trying to get the split PET/MR script and it doesn't exist, give - // them the default implementation. - // TODO This should be expanded into a default script repository function. - if (script == null && _scriptId.equalsIgnoreCase(PrearcDatabase.SPLIT_PETMR_SESSION_ID)) { - script = PrearcDatabase.DEFAULT_SPLIT_PETMR_SESSION_SCRIPT; + if (StringUtils.isNotBlank(_version)) { + //They're requesting a specific version of a specific script + return new StringRepresentation(MAPPER.writeValueAsString(_scriptService.getVersion(_scriptId, _version)), mediaType); } - - // have to check if it's null, or else it will return a StringRepresentation containing the word null instead of a 404 - if (script != null) { - return new StringRepresentation(MAPPER.writeValueAsString(script), mediaType); - } else { - return null; + else { + // They're requesting a specific script, so return that to them. + Script script = getScript(); + + // Here's a special case: if they're trying to get the split PET/MR script and it doesn't exist, give + // them the default implementation. + // TODO This should be expanded into a default script repository function. + if (script == null && _scriptId.equalsIgnoreCase(PrearcDatabase.SPLIT_PETMR_SESSION_ID)) { + script = PrearcDatabase.DEFAULT_SPLIT_PETMR_SESSION_SCRIPT; + } + + // have to check if it's null, or else it will return a StringRepresentation containing the word null instead of a 404 + if (script != null) { + return new StringRepresentation(MAPPER.writeValueAsString(script), mediaType); + } else { + return null; + } } } catch (IOException e) { throw new ResourceException(Status.SERVER_ERROR_INTERNAL, "An error occurred marshalling the script data to JSON", e); @@ -158,6 +166,7 @@ public class ScriptResource extends AutomationResource { columns.add("Script ID"); columns.add("Language"); columns.add("Description"); + //columns.add("Version"); XFTTable table = new XFTTable(); table.initTable(columns); @@ -167,6 +176,7 @@ public class ScriptResource extends AutomationResource { table.insertRowItems(script.getScriptId(), script.getLanguage(), script.getDescription()); + //script.getScriptVersion()); } return representTable(table, mediaType, params); @@ -213,6 +223,23 @@ public class ScriptResource extends AutomationResource { if (properties.containsKey("scriptId")) { properties.remove("scriptId"); } +// int previousMaxVersion = 0; +// try{ +// int version = Integer.parseInt(_scriptService.getByScriptId(_scriptId).getScriptVersion()); +// if(version>0){ +// previousMaxVersion=version; +// } +// } +// catch(Exception e){ +// _log.error("",e); +// } +// if (properties.containsKey("scriptVersion") && !properties.getProperty("scriptVersion").isEmpty()) { +// //properties.setProperty("scriptVersion", ""+(Integer.parseInt(properties.getProperty("scriptVersion"))+1)); +// properties.setProperty("scriptVersion", ""+(Integer.parseInt(properties.getProperty("scriptVersion")))); +// } +// else{ + //properties.setProperty("scriptVersion", ""+(previousMaxVersion+1)); +// } try { _runnerService.setScript(_scriptId, properties); @@ -225,9 +252,11 @@ public class ScriptResource extends AutomationResource { private static final Logger _log = LoggerFactory.getLogger(ScriptResource.class); private static final String SCRIPT_ID = "SCRIPT_ID"; + private static final String VERSION = "VERSION"; private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); private final ScriptService _scriptService; private final ScriptRunnerService _runnerService; private final String _scriptId; + private final String _version; } diff --git a/src/main/java/org/nrg/xnat/restlet/resources/ScriptVersionsResource.java b/src/main/java/org/nrg/xnat/restlet/resources/ScriptVersionsResource.java new file mode 100644 index 00000000..4d62c182 --- /dev/null +++ b/src/main/java/org/nrg/xnat/restlet/resources/ScriptVersionsResource.java @@ -0,0 +1,144 @@ +package org.nrg.xnat.restlet.resources; + +import org.apache.commons.lang.StringUtils; +import org.nrg.automation.entities.Script; +import org.nrg.automation.services.ScriptRunnerService; +import org.nrg.automation.services.ScriptService; +import org.nrg.xdat.XDAT; +import org.nrg.xdat.security.helpers.Roles; +import org.nrg.xft.XFTTable; +import org.nrg.xnat.helpers.prearchive.PrearcDatabase; +import org.restlet.Context; +import org.restlet.data.MediaType; +import org.restlet.data.Request; +import org.restlet.data.Response; +import org.restlet.data.Status; +import org.restlet.resource.Representation; +import org.restlet.resource.ResourceException; +import org.restlet.resource.StringRepresentation; +import org.restlet.resource.Variant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; + +public class ScriptVersionsResource extends AutomationResource { + + public ScriptVersionsResource(Context context, Request request, Response response) throws ResourceException { + super(context, request, response); + + getVariants().add(new Variant(MediaType.APPLICATION_JSON)); + getVariants().add(new Variant(MediaType.TEXT_HTML)); + getVariants().add(new Variant(MediaType.TEXT_XML)); + getVariants().add(new Variant(MediaType.TEXT_PLAIN)); + + _scriptService = XDAT.getContextService().getBean(ScriptService.class); + _runnerService = XDAT.getContextService().getBean(ScriptRunnerService.class); + + _scriptId = (String) getRequest().getAttributes().get(SCRIPT_ID); + + // If the user isn't a site admin, there's a limited set of operations they are permitted to perform. + if (!Roles.isSiteAdmin(user)) { + // You can't put or post or delete a script and you can't retrieve a specific script OTHER THAN the split + // PET/MR script, which is used by the upload applet. + if ((StringUtils.isNotBlank(_scriptId) && !_scriptId.equals(PrearcDatabase.SPLIT_PETMR_SESSION_ID))) { + _log.warn(getRequestContext("User " + user.getLogin() + " attempted to access forbidden script trigger template resources")); + response.setStatus(Status.CLIENT_ERROR_FORBIDDEN, "Only site admins can view or update script resources."); + throw new ResourceException(Status.CLIENT_ERROR_FORBIDDEN, "Only site admins can view or update script resources."); + } + } + + if (_log.isDebugEnabled()) { + _log.debug(getRequestContext("Servicing script request for user " + user.getLogin())); + } + } + + @Override + protected String getResourceType() { + return Script.class.getSimpleName(); + } + + @Override + protected String getResourceId() { + return _scriptId; + } + + @Override + public Representation represent(Variant variant) throws ResourceException { + final MediaType mediaType = overrideVariant(variant); + + if (StringUtils.isNotBlank(_scriptId)) { + try { + List<String> versions = _scriptService.getVersions(_scriptId); + +// // They're requesting a specific script, so return that to them. +// List<Script> script = getScripts(); +// +// // Here's a special case: if they're trying to get the split PET/MR script and it doesn't exist, give +// // them the default implementation. +// // TODO This should be expanded into a default script repository function. +// if (script == null && _scriptId.equalsIgnoreCase(PrearcDatabase.SPLIT_PETMR_SESSION_ID)) { +// script = new ArrayList<Script>(); +// script.add(PrearcDatabase.DEFAULT_SPLIT_PETMR_SESSION_SCRIPT); +// } + + // have to check if it's null, or else it will return a StringRepresentation containing the word null instead of a 404 + if (versions != null) { + return new StringRepresentation(MAPPER.writeValueAsString(versions), mediaType); + } else { + return null; + } + } catch (IOException e) { + throw new ResourceException(Status.SERVER_ERROR_INTERNAL, "An error occurred marshalling the script data to JSON", e); + } + } else { + // They're asking for list of available scripts, so give them that. + return listScripts(mediaType); + } + } + + /** + * Lists the scripts at the specified scope and entity ID. + * + * @return A representation of the scripts available at the specified scope and entity ID (if specified). + */ + private Representation listScripts(final MediaType mediaType) { + Hashtable<String, Object> params = new Hashtable<>(); + + ArrayList<String> columns = new ArrayList<>(); + columns.add("Script ID"); + columns.add("Language"); + columns.add("Description"); + columns.add("Version"); + + XFTTable table = new XFTTable(); + table.initTable(columns); + + final List<Script> scripts = _scriptService.getAll(); + for (final Script script : scripts) { + table.insertRowItems(script.getScriptId(), + script.getLanguage(), + script.getDescription()); +// script.getScriptVersion()); + } + + return representTable(table, mediaType, params); + } + + private List<Script> getScripts() { + return _runnerService.getScripts(_scriptId); + } + + private static final Logger _log = LoggerFactory.getLogger(ScriptVersionsResource.class); + + private static final String SCRIPT_ID = "SCRIPT_ID"; + private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + private final ScriptService _scriptService; + private final ScriptRunnerService _runnerService; + private final String _scriptId; +} diff --git a/src/main/webapp/scripts/xnat/app/scriptEditor.js b/src/main/webapp/scripts/xnat/app/scriptEditor.js index e18c92d8..2de23f98 100644 --- a/src/main/webapp/scripts/xnat/app/scriptEditor.js +++ b/src/main/webapp/scripts/xnat/app/scriptEditor.js @@ -45,6 +45,11 @@ var XNAT = getObject(XNAT||{}); return XNAT.url.restUrl('/data/automation/scripts/' + scriptId, params); } + // return script url with common parts pre-defined + function scriptVersionsURL( scriptId, params ){ + return XNAT.url.restUrl('/data/automation/scriptVersions/' + scriptId, params); + } + function langMenuOptions(langs){ langs = langs || scriptEditor.languages; @@ -81,8 +86,8 @@ var XNAT = getObject(XNAT||{}); scriptEditor.scriptIds.push(script['Script ID']); list += rowTemplate. - replace(/__SCRIPT_ID__/g, script['Script ID']). - replace(/__SCRIPT_DESCRIPTION__/g, XNAT.utils.escapeXML(script['Description'])); + replace(/__SCRIPT_ID__/g, script['Script ID']). + replace(/__SCRIPT_DESCRIPTION__/g, XNAT.utils.escapeXML(script['Description'])); }); scriptsTable.find('> tbody').html(list); scriptsTable.show(); @@ -138,6 +143,7 @@ var XNAT = getObject(XNAT||{}); var data = { content: ace.edit(editor_id).getSession().getValue(), description: $dialog.find('.script-description').val(), + scriptVersion: $dialog.find('.script-version').val(), language: $dialog.find('input.language').val() || '' }; @@ -167,57 +173,18 @@ var XNAT = getObject(XNAT||{}); } var counter = 0; - - // load script into the editor - function loadEditor($dialog, json){ - - json = json || {}; - - console.log(json); - - var scriptId = json.scriptId || ''; - var lang = json.language || 'groovy'; - var time = json.timestamp || ''; - - $dialog.find('.id').val(json.id || ''); - $dialog.find('.scriptId').val(scriptId); - $dialog.find('.language').val(lang); - $dialog.find('.timestamp').val(time); - $dialog.find('.script-description').val(json.description || ''); - - if (scriptId){ - $dialog.find('.script-id-text').html(scriptId); - $dialog.find('.script-id-input').remove(); - //$dialog.find('.script-id-input').val(scriptId); - } - - var $wrapper = $dialog.find('.editor-wrapper'); - - // make sure the editor wrapper is empty - $wrapper.empty(); - - // create an entirely new editor div - var _editor = document.createElement('div'); - _editor.id = 'script-' + (scriptId || (json.id||++counter)) + '-content'; - _editor.className = 'editor-content'; - _editor.innerHTML = XNAT.utils.escapeXML(json.content) || ''; - _editor.style = 'position:absolute;top:0;right:0;bottom:0;left:0;border: 1px solid #ccc'; - - // put the new editor div in the wrapper - $wrapper.append(_editor); - - // save the id to outer scope for other functions - scriptEditor.editor_id = _editor.id; - - var aceEditor = ace.edit(_editor); - aceEditor.setTheme("ace/theme/eclipse"); - aceEditor.getSession().setMode("ace/mode/" + stringLower(lang)); - - } - - // open xmodal dialog for script editing function renderEditor( json ){ + //var fullJson = json || {}; + //json = {}; + //var largestVersion = -1; + //var arrayLength = fullJson.length; + //for (var i = 0; i < arrayLength; i++) { + // if(fullJson[i].scriptVersion>largestVersion){ + // largestVersion = fullJson[i].scriptVersion; + // json = fullJson[i]; + // } + //} var scriptId = json.scriptId || ''; var lang = json.language || 'groovy'; @@ -236,7 +203,7 @@ var XNAT = getObject(XNAT||{}); opts.footerContent = '<span style="color:#555;">'; if (time){ opts.footerContent += - 'last modified: ' + (new Date(time)).toString(); + 'last modified: ' + (new Date(time)).toString(); } opts.footerContent += '</span>'; opts.buttons = { @@ -259,51 +226,123 @@ var XNAT = getObject(XNAT||{}); var $dialog = obj.$modal; - loadEditor($dialog, json); + $dialog.find('.id').val(json.id || ''); + $dialog.find('.scriptId').val(scriptId); + $dialog.find('.language').val(lang); + $dialog.find('.timestamp').val(time); + $dialog.find('.script-description').val(json.description || ''); + + var currScriptVersion = 1; + if(scriptId) { + //xhr.getJSON(XNAT.url.restUrl('/data/automation/scriptVersions/' + scriptId), function (jsonVersions) { + // for(var key in jsonVersions){ + // if(key!="contains") { + // var currScript = jsonVersions[key]; + // currScriptVersion = currScript.scriptVersion; + // $('#script-version') + // .append($("<option></option>") + // .attr("value", currScriptVersion) + // .text(currScriptVersion)); + // } + // } + // $('#script-version').val(currScriptVersion); + //}); + xhr.getJSON(XNAT.url.restUrl('/data/automation/scriptVersions/' + scriptId), function (versionsList) { + var versCounter = 1; + for(var vers in versionsList){ + if(vers!="contains") { + currScriptVersion = versionsList[vers]; + $('#script-version') + .append($("<option></option>") + .attr("value", currScriptVersion) + .text(versCounter)); + versCounter = versCounter+1; + } + } + $('#script-version').val(currScriptVersion); + }); + } + else{ + $('#script-version') + .append($("<option></option>") + .attr("value", "1") + .attr("selected","selected") + .text("1")); + } + //$("div.script-version select").val(currScriptVersion); + $('#script-version').change(function(){ + var selectedVersion = $('#script-version')[0].value; + xhr.getJSON(XNAT.url.restUrl('/data/automation/scripts/' + scriptId + '/'+ selectedVersion), function (versionObj) { + json = versionObj; + var $dialog = obj.$modal; + + //$dialog.find('.id').val(json.id || ''); + $dialog.find('.scriptId').val(scriptId || ''); + $dialog.find('.language').val(json.language || ''); + $dialog.find('.timestamp').val(json.timestamp || ''); + $dialog.find('.script-description').val(json.description || ''); + $dialog.find('.script-version').val(selectedVersion || ''); + + var $wrapper = $dialog.find('.editor-wrapper'); + + // make sure the editor wrapper is empty + $wrapper.empty(); + + // create an entirely new editor div + var _editor = document.createElement('div'); + _editor.id = 'script-' + (scriptId || (json.id||++counter)) + '-content'; + _editor.className = 'editor-content'; + _editor.innerHTML = XNAT.utils.escapeXML(json.content) || ''; + _editor.style = 'position:absolute;top:0;right:0;bottom:0;left:0;border: 1px solid #ccc'; + + // put the new editor div in the wrapper + $wrapper.append(_editor); + + // save the id to outer scope for other functions + scriptEditor.editor_id = _editor.id; + + var aceEditor = ace.edit(_editor); + aceEditor.setTheme("ace/theme/eclipse"); + aceEditor.getSession().setMode("ace/mode/" + stringLower(lang)); + }); + + }); - /* + if (scriptId){ + $dialog.find('.script-id-text').html(scriptId); + $dialog.find('.script-id-input').remove(); + //$dialog.find('.script-id-input').val(scriptId); + } - // VERSION MENU START - // NEEDS EDITING FOR IMPLEMENTATION + var $wrapper = $dialog.find('.editor-wrapper'); - var $versionMenu = $dialog.find('.script-version'); + // make sure the editor wrapper is empty + $wrapper.empty(); - ////////////////////////////////////////////////////////////////////// - // populate the 'version' menu - // THIS IS A PLACEHOLDER - // REPLACE WITH VERSIONS FROM 'json' - ////////////////////////////////////////////////////////////////////// - $('#scripts-table').find('[data-script-id]').each(function(){ - var id = $(this).data('script-id'); - var $option = $(document.createElement('option')); - //$option.attr('data-url', scriptURL(id)); - $option.html(id); - $option.val(id); - $versionMenu.append($option); - }); - ////////////////////////////////////////////////////////////////////// + // create an entirely new editor div + var _editor = document.createElement('div'); + _editor.id = 'script-' + (scriptId || (json.id||++counter)) + '-content'; + _editor.className = 'editor-content'; + _editor.innerHTML = XNAT.utils.escapeXML(json.content) || ''; + _editor.style = 'position:absolute;top:0;right:0;bottom:0;left:0;border: 1px solid #ccc'; - // put select menu handler here - $versionMenu.on('change', function(){ - xhr.getJSON(scriptURL(this.value), function(data){ - loadEditor($dialog, data) - }); - }); + // put the new editor div in the wrapper + $wrapper.append(_editor); - // VERSION MENU END + // save the id to outer scope for other functions + scriptEditor.editor_id = _editor.id; - */ + var aceEditor = ace.edit(_editor); + aceEditor.setTheme("ace/theme/eclipse"); + aceEditor.getSession().setMode("ace/mode/" + stringLower(lang)); }; opts.afterShow = function(obj){ if (!scriptId){ obj.$modal.find('.script-id-input').focus().select(); } - // TODO: prompt user to save if they could lose unsaved changes }; - xmodal.open(opts); - } // open dialog to choose language @@ -402,8 +441,11 @@ var XNAT = getObject(XNAT||{}); xmodal.closeAll(); } }; + var csrfParam = { + XNAT_CSRF: csrfToken + }; - xhr.delete(scriptURL(scriptId), { + xhr.delete(scriptURL(scriptId, csrfParam), { success: function(){ xmodal.message(successDialog); }, diff --git a/src/main/webapp/xnat-templates/screens/EditScript.vm b/src/main/webapp/xnat-templates/screens/EditScript.vm index 161e0523..57b4fc97 100755 --- a/src/main/webapp/xnat-templates/screens/EditScript.vm +++ b/src/main/webapp/xnat-templates/screens/EditScript.vm @@ -67,6 +67,9 @@ <tr> <td><label for="scriptId"><strong>Script ID:</strong></label></td><td><input type="text" name="scriptId" id="scriptId" value="$!script.scriptId"/></td> </tr> + <tr> + <td><label for="scriptVersion"><strong>Version:</strong></label></td><td><input type="text" name="scriptVersion" id="scriptVersion" size="80" value="$!script.scriptVersion" readonly/></td> + </tr> <tr> <td><label for="description"><strong>Description:</strong></label></td><td><input type="text" name="description" id="description" size="80" value="$!script.description"/></td> </tr> diff --git a/src/main/webapp/xnat-templates/screens/Scripts.vm b/src/main/webapp/xnat-templates/screens/Scripts.vm index db72d0dc..9186484e 100644 --- a/src/main/webapp/xnat-templates/screens/Scripts.vm +++ b/src/main/webapp/xnat-templates/screens/Scripts.vm @@ -27,7 +27,7 @@ <div class="yui-skin-sam"> - <div id="tp_fm" style="display:none"></div> +## <div id="tp_fm" style="display:none"></div> #if($data.getSession().getAttribute("user").checkRole("Administrator")) @@ -133,7 +133,7 @@ </table> <br> <b style="padding:0 8px;">Create a site-wide Event Handler: </b> - <button type="button" id="manage_event_handlers" class="btn1" style="font-size:12px;" title="manage event handlers">Manage Event Handlers</button> + <button type="button" id="add_event_handler" class="btn1" style="font-size:12px;" title="add an event handler">Add Event Handler</button> </div> @@ -184,7 +184,7 @@ <thead> <th>Script ID</th> <th width="50%">Description</th> - ## <th>Version</th> + ##<th>Version</th> <th> </th> ## <th> </th> ## <th> </th> @@ -314,12 +314,12 @@ <td><b>Description: </b> </td> <td><input type="text" name="script-description" class="script-description" size="80" value=""></td> </tr> - <!-- VERSION MENU <tr> - <td><b>Load Version: </b> </td> - <td><select name="script-version" class="script-version"></select></td> + <td><b>Script Version: </b> </td> + <td><select id="script-version" class="script-version"> + <option value="!">Select a Version</option> + </select></td> </tr> - --> </table> <br> <div class="editor-wrapper" style="width:840px;height:482px;position:relative;"> -- GitLab