diff --git a/build.gradle b/build.gradle index 63ac93f199ff13d7dc16e4fc00fa1279e9cf75b0..0d0a783917c38a4eed3eb0921b2142de2f4fd8ac 100644 --- a/build.gradle +++ b/build.gradle @@ -105,6 +105,8 @@ configurations { all*.exclude group: 'avalon-logkit' all*.exclude group: 'com.metaparadigm' all*.exclude group: 'com.sun.mail' + all*.exclude group: 'commons-dbcp' + all*.exclude group: 'commons-pool' all*.exclude group: 'excalibur-component' all*.exclude group: 'excalibur-instrument' all*.exclude group: 'excalibur-logger' @@ -221,6 +223,7 @@ dependencies { compile "org.json:json:20151123" compile "xerces:xercesImpl:2.11.0" + compile "commons-beanutils:commons-beanutils:1.9.2" compile "commons-codec:commons-codec:1.10" compile "commons-collections:commons-collections:3.2.2" compile "commons-configuration:commons-configuration:1.10" @@ -228,7 +231,6 @@ dependencies { compile "commons-discovery:commons-discovery:0.5" compile "commons-fileupload:commons-fileupload:1.3.1" compile "commons-net:commons-net:3.4" - compile "commons-pool:commons-pool:1.6" compile "org.apache.commons:commons-email:1.4" compile "org.apache.commons:commons-math:2.2" diff --git a/src/main/java/org/nrg/dcm/DicomSCPManager.java b/src/main/java/org/nrg/dcm/DicomSCPManager.java index 1503473bafc9884ce692669845ffc9e7dc64a5ab..dc51a81a6350c54110b9f4bf2b725cb686ae59be 100644 --- a/src/main/java/org/nrg/dcm/DicomSCPManager.java +++ b/src/main/java/org/nrg/dcm/DicomSCPManager.java @@ -10,22 +10,14 @@ */ package org.nrg.dcm; -import com.google.common.base.Joiner; -import org.apache.commons.lang3.StringUtils; import org.nrg.config.services.SiteConfigurationService; import org.nrg.dcm.preferences.DicomSCPInstance; import org.nrg.dcm.preferences.DicomSCPPreference; import org.nrg.framework.exceptions.NrgServiceError; import org.nrg.framework.exceptions.NrgServiceException; import org.nrg.framework.exceptions.NrgServiceRuntimeException; -import org.nrg.xdat.om.XnatProjectdata; -import org.nrg.xnat.DicomObjectIdentifier; -import org.nrg.xnat.utils.XnatUserProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; import javax.annotation.PreDestroy; import javax.inject.Inject; @@ -34,18 +26,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.Executors; -public class DicomSCPManager implements ApplicationContextAware { - - public DicomSCPManager(final XnatUserProvider provider) throws IOException { - _provider = provider; - } - - @Override - public void setApplicationContext(final ApplicationContext context) throws BeansException { - _context = context; - } +public class DicomSCPManager { @PreDestroy public void shutdown() { @@ -53,173 +35,141 @@ public class DicomSCPManager implements ApplicationContextAware { stopDicomSCPs(); } - /** - * Sets the preferences for the DICOM SCP manager. - * - * @param preferences The preferences to set. - */ - @SuppressWarnings("unused") - public void setPreferences(final DicomSCPPreference preferences) { - _preferences = preferences; - for (final DicomSCPInstance instance : preferences.getDicomSCPInstances().values()) { - try { - createDicomScpFromInstance(instance); - } catch (IOException e) { - _log.error("An error occurred trying to create the DICOM SCP instance " + instance.toString()); - } - } - } - - public void create(final DicomSCPInstance instance) throws IOException, NrgServiceException { + public DicomSCP create(final DicomSCPInstance instance) throws NrgServiceException { final String scpId = instance.getScpId(); if (_preferences.hasDicomSCPInstance(scpId)) { throw new NrgServiceException(NrgServiceError.ConfigurationError, "There is already a DICOM SCP instance with the ID " + scpId); } - _preferences.setDicomSCPInstance(instance); - createDicomScpFromInstance(instance); + try { + _preferences.setDicomSCPInstance(instance); + return _preferences.getDicomSCP(scpId); + } catch (IOException e) { + throw new NrgServiceRuntimeException(NrgServiceError.Unknown, "Unable to create DICOM SCP: " + instance.getAeTitle() + ":" + instance.getPort(), e); + } } public void delete(final String scpId) throws NrgServiceException { if (!_preferences.hasDicomSCPInstance(scpId)) { throw new NrgServiceException(NrgServiceError.UnknownEntity, "There is no DICOM SCP instance with the ID " + scpId); } - stopDicomSCP(scpId); - _dicomSCPs.remove(scpId); _preferences.deleteDicomSCPInstance(scpId); } - public void startOrStopDicomSCPAsDictatedByConfiguration() { - final boolean enableDicomReceiver = _siteConfigurationService.getBoolSiteConfigurationProperty("enableDicomReceiver", true); - if (enableDicomReceiver) { - startDicomSCPs(); - } else { - stopDicomSCPs(); + public List<DicomSCPInstance> getDicomSCPInstances() { + return new ArrayList<>(_preferences.getDicomSCPInstances().values()); + } + + public void setDicomSCPInstance(final DicomSCPInstance instance) { + try { + _preferences.setDicomSCPInstance(instance); + } catch (IOException e) { + throw new NrgServiceRuntimeException(NrgServiceError.Unknown, "Unable to update DICOM SCP: " + instance.getAeTitle() + ":" + instance.getPort(), e); } } - public void startDicomSCPs() { - for (final String scpId : _dicomSCPs.keySet()) { - final DicomSCPInstance instance = _preferences.getDicomSCPInstance(scpId); + public List<String> startOrStopDicomSCPAsDictatedByConfiguration() { + final boolean enableDicomReceiver = _siteConfigurationService.getBoolSiteConfigurationProperty("enableDicomReceiver", true); + return enableDicomReceiver ? startDicomSCPs() : stopDicomSCPs(); + } + + public List<String> startDicomSCPs() { + final List<String> started = new ArrayList<>(); + for (final DicomSCPInstance instance : _preferences.getDicomSCPInstances().values()) { if (instance.isEnabled()) { - startDicomSCP(scpId); + startDicomSCP(instance); + started.add(instance.getScpId()); } } + return started; } public void startDicomSCP(final String scpId) { - final DicomSCP dicomSCP = getDicomSCP(scpId); - try { - dicomSCP.start(); - final DicomSCPInstance instance = _preferences.getDicomSCPInstance(scpId); - if (!instance.isEnabled()) { - enableDicomSCP(scpId); - } - } catch (IOException e) { - throw new NrgServiceRuntimeException(NrgServiceError.Unknown, "Unable to start DICOM SCP: " + Joiner.on("/").join(dicomSCP.getAEs()) + ":" + dicomSCP.getPort(), e); - } + startDicomSCP(_preferences.getDicomSCPInstance(scpId)); } - public void enableDicomSCP(final String scpId) throws IOException { - final DicomSCPInstance instance = _preferences.getDicomSCPInstance(scpId); - if (!instance.isEnabled()) { - instance.setEnabled(true); - _preferences.setDicomSCPInstance(instance); - } - final DicomSCP dicomSCP = _dicomSCPs.get(scpId); - if (!dicomSCP.isStarted()) { - dicomSCP.start(); + public List<String> stopDicomSCPs() { + final List<String> stopped = new ArrayList<>(); + for (final DicomSCP dicomSCP : _preferences.getDicomSCPs()) { + if (dicomSCP.isStarted()) { + dicomSCP.stop(); + stopped.add(dicomSCP.getScpId()); + } } + return stopped; } - public void disableDicomSCP(final String scpId) throws IOException { + public void stopDicomSCP(final String scpId) { final DicomSCPInstance instance = _preferences.getDicomSCPInstance(scpId); - if (instance.isEnabled()) { - instance.setEnabled(false); - _preferences.setDicomSCPInstance(instance); + if (instance == null) { + throw new NrgServiceRuntimeException(NrgServiceError.UnknownEntity, "Couldn't find the DICOM SCP instance identified by " + scpId); } - final DicomSCP dicomSCP = _dicomSCPs.get(scpId); - if (dicomSCP.isStarted()) { - dicomSCP.stop(); + try { + final DicomSCP dicomSCP = _preferences.getDicomSCP(scpId); + if (dicomSCP != null) { + if (dicomSCP.isStarted()) { + dicomSCP.stop(); + } + } + } catch (IOException e) { + throw new NrgServiceRuntimeException(NrgServiceError.Unknown, "Unable to stop DICOM SCP: " + instance.getAeTitle() + ":" + instance.getPort(), e); } } - public void stopDicomSCPs() { - for (final String scpId : _dicomSCPs.keySet()) { - stopDicomSCP(scpId); + public void enableDicomSCP(final String scpId) { + final DicomSCPInstance instance = _preferences.getDicomSCPInstance(scpId); + try { + if (!instance.isEnabled()) { + instance.setEnabled(true); + _preferences.setDicomSCPInstance(instance); + } + final DicomSCP dicomSCP = _preferences.getDicomSCP(scpId); + if (!dicomSCP.isStarted()) { + dicomSCP.start(); + } + } catch (IOException e) { + throw new NrgServiceRuntimeException(NrgServiceError.Unknown, "Unable to enable DICOM SCP: " + instance.getAeTitle() + ":" + instance.getPort(), e); } } - public void stopDicomSCP(final String scpId) { - final DicomSCP dicomSCP = _dicomSCPs.get(scpId); - if (dicomSCP == null) { - throw new NrgServiceRuntimeException(NrgServiceError.UnknownEntity, "Couldn't find the DICOM SCP instance identified by " + scpId); - } - if (dicomSCP.isStarted()) { - dicomSCP.stop(); + public void disableDicomSCP(final String scpId) { + final DicomSCPInstance instance = _preferences.getDicomSCPInstance(scpId); + try { + if (instance.isEnabled()) { + instance.setEnabled(false); + _preferences.setDicomSCPInstance(instance); + } + final DicomSCP dicomSCP = _preferences.getDicomSCP(scpId); + if (dicomSCP.isStarted()) { + dicomSCP.stop(); + } + } catch (IOException e) { + throw new NrgServiceRuntimeException(NrgServiceError.Unknown, "Unable to disable DICOM SCP: " + instance.getAeTitle() + ":" + instance.getPort(), e); } } public Map<String, Boolean> areDicomSCPsStarted() { final Map<String, Boolean> statuses = new HashMap<>(); - for (final String scpId : _dicomSCPs.keySet()) { - statuses.put(scpId, isDicomSCPStarted(scpId)); + for (final DicomSCP dicomSCP : _preferences.getDicomSCPs()) { + statuses.put(dicomSCP.getScpId(), dicomSCP.isStarted()); } return statuses; } - public boolean isDicomSCPStarted(final String scpId) { - final DicomSCP dicomSCP = _dicomSCPs.get(scpId); - if (dicomSCP == null) { - throw new NrgServiceRuntimeException(NrgServiceError.UnknownEntity, "Couldn't find the DICOM SCP instance identified by " + scpId); - } - return dicomSCP.isStarted(); - } - public boolean hasDicomSCP(final String scpId) { - return _dicomSCPs.containsKey(scpId); - } - - public List<DicomSCPInstance> getDicomSCPInstances() { - return new ArrayList<>(_preferences.getDicomSCPInstances().values()); + return _preferences.hasDicomSCPInstance(scpId); } public DicomSCPInstance getDicomSCPInstance(final String scpId) { return _preferences.getDicomSCPInstance(scpId); } - private DicomSCP getDicomSCP(final String scpId) { - final DicomSCP dicomSCP = _dicomSCPs.get(scpId); - if (dicomSCP == null) { - throw new NrgServiceRuntimeException(NrgServiceError.UnknownEntity, "Couldn't find the DICOM SCP instance identified by " + scpId); - } - return dicomSCP; - } - - private void createDicomScpFromInstance(final DicomSCPInstance instance) throws IOException { - final String scpId = instance.getScpId(); - final DicomSCP dicomScp = DicomSCP.create(scpId, Executors.newCachedThreadPool(), instance.getPort(), _provider, instance.getAeTitle(), getIdentifier(instance.getIdentifier()), getDicomFileNamer(instance.getFileNamer())); - _dicomSCPs.put(scpId, dicomScp); - if (instance.isEnabled()) { - dicomScp.start(); - } - } - - private DicomObjectIdentifier<XnatProjectdata> getIdentifier(final String identifier) { - final DicomObjectIdentifier bean = StringUtils.isBlank(identifier) ? _context.getBean(DicomObjectIdentifier.class) : _context.getBean(identifier, DicomObjectIdentifier.class); - if (bean == null) { - throw new NrgServiceRuntimeException(NrgServiceError.Unknown, "Could not find a DICOM object identifier with the ID " + identifier); - } - //noinspection unchecked - return (DicomObjectIdentifier<XnatProjectdata>) bean; - } - - private DicomFileNamer getDicomFileNamer(final String identifier) { - //noinspection unchecked - final DicomFileNamer bean = StringUtils.isBlank(identifier) ? _context.getBean(DicomFileNamer.class) : _context.getBean(identifier, DicomFileNamer.class); - if (bean == null) { - throw new NrgServiceRuntimeException(NrgServiceError.Unknown, "Could not find a DICOM object identifier with the ID " + identifier); + private void startDicomSCP(final DicomSCPInstance instance) { + try { + final DicomSCP dicomSCP = _preferences.getDicomSCP(instance.getScpId()); + dicomSCP.start(); + } catch (IOException e) { + throw new NrgServiceRuntimeException(NrgServiceError.Unknown, "Unable to start DICOM SCP: " + instance.getAeTitle() + ":" + instance.getPort(), e); } - return bean; } private static final Logger _log = LoggerFactory.getLogger(DicomSCPManager.class); @@ -229,8 +179,4 @@ public class DicomSCPManager implements ApplicationContextAware { @Inject private DicomSCPPreference _preferences; - - private final Map<String, DicomSCP> _dicomSCPs = new HashMap<>(); - private ApplicationContext _context; - private final XnatUserProvider _provider; } diff --git a/src/main/java/org/nrg/dcm/id/TemplatizedDicomFileNamer.java b/src/main/java/org/nrg/dcm/id/TemplatizedDicomFileNamer.java index ecddac66373024b7883855cdd266c916cfc9df25..62bc623f813ad5f0cb54aaf22696e8c6875566e2 100644 --- a/src/main/java/org/nrg/dcm/id/TemplatizedDicomFileNamer.java +++ b/src/main/java/org/nrg/dcm/id/TemplatizedDicomFileNamer.java @@ -10,18 +10,9 @@ */ package org.nrg.dcm.id; -import java.io.StringReader; -import java.io.StringWriter; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.apache.commons.lang.StringUtils; +import com.google.common.base.Functions; +import com.google.common.collect.Lists; +import org.apache.commons.lang3.StringUtils; import org.apache.velocity.Template; import org.apache.velocity.VelocityContext; import org.apache.velocity.runtime.RuntimeServices; @@ -33,8 +24,11 @@ import org.nrg.dcm.DicomFileNamer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.base.Functions; -import com.google.common.collect.Lists; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class TemplatizedDicomFileNamer implements DicomFileNamer { @@ -42,8 +36,8 @@ public class TemplatizedDicomFileNamer implements DicomFileNamer { private static final String SUFFIX = ".dcm"; private static final String HASH_PREFIX = "Hash"; - private static final Logger _log = LoggerFactory.getLogger(TemplatizedDicomFileNamer.class); - public static final String HASH_DELIMITER = "With"; + private static final Logger _log = LoggerFactory.getLogger(TemplatizedDicomFileNamer.class); + private static final String HASH_DELIMITER = "With"; public TemplatizedDicomFileNamer(final String naming) throws Exception { if (_log.isDebugEnabled()) { @@ -61,31 +55,21 @@ public class TemplatizedDicomFileNamer implements DicomFileNamer { * @param dicomObject The DICOM object for which the name should be calculated. * @return The generated file name from the variable values extracted from the DICOM object. */ - public String makeFileName(DicomObject dicomObject) { - Map<String, String> values = new HashMap<>(); + public String makeFileName(final DicomObject dicomObject) { + final VelocityContext context = new VelocityContext(); + final Map<String, String> values = new HashMap<>(); for (final String variable : _variables) { if (!variable.startsWith(HASH_PREFIX)) { final String tagValue = dicomObject.getString(Tag.forName(variable)); - values.put(variable, tagValue == null ? "no-value-for-" + variable : tagValue); + final String value = StringUtils.isEmpty(tagValue) ? "no-value-for-" + variable : tagValue; + context.put(variable, value); + values.put(variable, value); } } - return makeFileName(values); - } - - /** - * Makes the file name from the given variables. - * @param values The various extracted variable values. - * @return The generated file name from the given variable values. - */ - public String makeFileName(Map<String, String> values) { - VelocityContext context = new VelocityContext(); - for (Map.Entry<String, String> value : values.entrySet()) { - context.put(value.getKey(), value.getValue()); - } for (final Map.Entry<String, List<String>> hash : _hashes.entrySet()) { context.put(hash.getKey(), calculateHashString(hash.getValue(), values)); } - StringWriter writer = new StringWriter(); + final StringWriter writer = new StringWriter(); try { getTemplate().merge(context, writer); } catch (Exception exception) { @@ -132,10 +116,10 @@ public class TemplatizedDicomFileNamer implements DicomFileNamer { runtimeServices.init(); StringReader reader = new StringReader(_naming); SimpleNode node = runtimeServices.parse(reader, "naming"); - Template template = new Template(); - template.setRuntimeServices(runtimeServices); - template.setData(node); - template.initDocument(); + _template = new Template(); + _template.setRuntimeServices(runtimeServices); + _template.setData(node); + _template.initDocument(); } } return _template; @@ -160,14 +144,14 @@ public class TemplatizedDicomFileNamer implements DicomFileNamer { * @return All of the hashes in the template. */ private Map<String, List<String>> initializeHashes() { - Map<String, List<String>> hashes = new HashMap<>(); - Set<String> hashedVariables = new HashSet<>(); + final Map<String, List<String>> hashes = new HashMap<>(); + final Set<String> hashedVariables = new HashSet<>(); for (final String variable : _variables) { if (variable.startsWith(HASH_PREFIX)) { if (!variable.contains(HASH_DELIMITER)) { throw new RuntimeException("You can't specify a " + HASH_PREFIX + " without specifying at least two DICOM header values joined by the " + HASH_DELIMITER + " delimiter."); } - List<String> variables = Arrays.asList(variable.substring(4).split(HASH_DELIMITER)); + final List<String> variables = Arrays.asList(variable.substring(4).split(HASH_DELIMITER)); hashes.put(variable, variables); hashedVariables.addAll(variables); } diff --git a/src/main/java/org/nrg/dcm/preferences/DicomSCPPreference.java b/src/main/java/org/nrg/dcm/preferences/DicomSCPPreference.java index 78ab6aaf7878a3538339c6ba9c497729480327b1..a184ae50e05464363f16c36f998a5e0bd7cfb547 100644 --- a/src/main/java/org/nrg/dcm/preferences/DicomSCPPreference.java +++ b/src/main/java/org/nrg/dcm/preferences/DicomSCPPreference.java @@ -1,17 +1,42 @@ package org.nrg.dcm.preferences; +import org.apache.commons.lang3.StringUtils; +import org.nrg.dcm.DicomFileNamer; +import org.nrg.dcm.DicomSCP; +import org.nrg.framework.exceptions.NrgServiceError; +import org.nrg.framework.exceptions.NrgServiceRuntimeException; import org.nrg.prefs.annotations.NrgPreference; import org.nrg.prefs.annotations.NrgPreferenceBean; import org.nrg.prefs.beans.AbstractPreferenceBean; import org.nrg.prefs.exceptions.InvalidPreferenceName; +import org.nrg.xdat.om.XnatProjectdata; +import org.nrg.xnat.DicomObjectIdentifier; +import org.nrg.xnat.utils.XnatUserProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import javax.inject.Inject; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; @NrgPreferenceBean(toolId = "dicomScpManager", toolName = "DICOM SCP Manager", description = "Manages configuration of the various DICOM SCP endpoints on the XNAT system.") -public class DicomSCPPreference extends AbstractPreferenceBean { +public class DicomSCPPreference extends AbstractPreferenceBean implements ApplicationContextAware { + + /** + * {@inheritDoc} + */ + @Override + public void setApplicationContext(final ApplicationContext context) throws BeansException { + _context = context; + } + public boolean hasDicomSCPInstance(final String scpId) { return getDicomSCPInstances().containsKey(scpId); } @@ -26,21 +51,71 @@ public class DicomSCPPreference extends AbstractPreferenceBean { } public void setDicomSCPInstance(final DicomSCPInstance instance) throws IOException { + final String scpId = instance.getScpId(); + deleteDicomSCP(scpId); try { - set(serialize(instance), PREF_ID, instance.getScpId()); + set(serialize(instance), PREF_ID, scpId); } catch (InvalidPreferenceName invalidPreferenceName) { - _log.info("Got an invalidate preference name error for " + instance.getScpId()); + _log.info("Got an invalidate preference name error for " + scpId); } + _dicomSCPs.put(scpId, getDicomSCP(scpId)); } public void deleteDicomSCPInstance(final String scpId) { - final String instanceId = getNamespacedPropertyId(PREF_ID, scpId); + deleteDicomSCP(scpId); try { - delete(instanceId); + delete(PREF_ID, scpId); } catch (InvalidPreferenceName invalidPreferenceName) { _log.info("Got an invalidate preference name error trying to delete DICOM SCP instance " + scpId); } } - private static final Logger _log = LoggerFactory.getLogger(DicomSCPInstance.class); + + public List<DicomSCP> getDicomSCPs() { + return new ArrayList<>(_dicomSCPs.values()); + } + + public DicomSCP getDicomSCP(final String scpId) throws IOException { + if (!hasDicomSCPInstance(scpId)) { + throw new NrgServiceRuntimeException(NrgServiceError.UnknownEntity, "There is no definition for the DICOM SCP with ID " + scpId); + } + if (!_dicomSCPs.containsKey(scpId)) { + final DicomSCPInstance instance = getDicomSCPInstance(scpId); + _dicomSCPs.put(scpId, DicomSCP.create(scpId, Executors.newCachedThreadPool(), instance.getPort(), _provider, instance.getAeTitle(), getIdentifier(instance.getIdentifier()), getDicomFileNamer(instance.getFileNamer()))); + } + return _dicomSCPs.get(scpId); + } + + private void deleteDicomSCP(final String scpId) { + if (_dicomSCPs.containsKey(scpId)) { + final DicomSCP deleted = _dicomSCPs.remove(scpId); + deleted.stop(); + } + } + + private DicomObjectIdentifier<XnatProjectdata> getIdentifier(final String identifier) { + final DicomObjectIdentifier bean = StringUtils.isBlank(identifier) ? _context.getBean(DicomObjectIdentifier.class) : _context.getBean(identifier, DicomObjectIdentifier.class); + if (bean == null) { + throw new NrgServiceRuntimeException(NrgServiceError.Unknown, "Could not find a DICOM object identifier with the ID " + identifier); + } + //noinspection unchecked + return (DicomObjectIdentifier<XnatProjectdata>) bean; + } + + private DicomFileNamer getDicomFileNamer(final String identifier) { + //noinspection unchecked + final DicomFileNamer bean = StringUtils.isBlank(identifier) ? _context.getBean(DicomFileNamer.class) : _context.getBean(identifier, DicomFileNamer.class); + if (bean == null) { + throw new NrgServiceRuntimeException(NrgServiceError.Unknown, "Could not find a DICOM object identifier with the ID " + identifier); + } + return bean; + } + + private static final Logger _log = LoggerFactory.getLogger(DicomSCPInstance.class); private static final String PREF_ID = "dicomSCPInstances"; + + @Inject + private XnatUserProvider _provider; + + private ApplicationContext _context; + private final Map<String, DicomSCP> _dicomSCPs = new HashMap<>(); } diff --git a/src/main/java/org/nrg/xapi/rest/AbstractXnatRestApi.java b/src/main/java/org/nrg/xapi/rest/AbstractXnatRestApi.java new file mode 100644 index 0000000000000000000000000000000000000000..1c2e5618ccf9f84c1fe3c419d581adfa64122139 --- /dev/null +++ b/src/main/java/org/nrg/xapi/rest/AbstractXnatRestApi.java @@ -0,0 +1,39 @@ +package org.nrg.xapi.rest; + +import org.nrg.xdat.security.XDATUser; +import org.nrg.xft.security.UserI; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Provides basic functions for integrating Spring REST controllers with XNAT. + */ +public abstract class AbstractXnatRestApi { + protected HttpStatus isPermitted(String id) { + UserI sessionUser = getSessionUser(); + if (sessionUser == null) { + return HttpStatus.UNAUTHORIZED; + } + if ((sessionUser.getUsername().equals(id)) || (isPermitted() == null)) { + return null; + } + return HttpStatus.FORBIDDEN; + } + + protected UserI getSessionUser() { + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if ((principal instanceof UserI)) { + return (UserI) principal; + } + return null; + } + + protected HttpStatus isPermitted() { + UserI sessionUser = getSessionUser(); + if ((sessionUser instanceof XDATUser)) { + return ((XDATUser) sessionUser).isSiteAdmin() ? null : HttpStatus.FORBIDDEN; + } + + return null; + } +} diff --git a/src/main/java/org/nrg/xapi/rest/dicomscp/DicomSCPApi.java b/src/main/java/org/nrg/xapi/rest/dicomscp/DicomSCPApi.java new file mode 100644 index 0000000000000000000000000000000000000000..6ef05d7cabeb205819782043b12951ba85dc92dd --- /dev/null +++ b/src/main/java/org/nrg/xapi/rest/dicomscp/DicomSCPApi.java @@ -0,0 +1,168 @@ +package org.nrg.xapi.rest.dicomscp; + +import io.swagger.annotations.*; +import org.nrg.dcm.DicomSCPManager; +import org.nrg.dcm.preferences.DicomSCPInstance; +import org.nrg.framework.exceptions.NrgServiceException; +import org.nrg.xapi.rest.NotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.inject.Inject; +import java.util.List; + +// @XnatRestlet({"/services/dicomscp", "/services/dicomscp/instance/{SCP_ID}", "/services/dicomscp/instance/{SCP_ID}/{ACTION}", "/services/dicomscp/{ACTION}"}) + +@Api(description = "XNAT DICOM SCP management API") +@RestController +@RequestMapping(value = "/dicomscp") +public class DicomSCPApi extends org.nrg.xapi.rest.AbstractXnatRestApi { + private static final Logger _log = LoggerFactory.getLogger(DicomSCPApi.class); + + @ApiOperation(value = "Get list of all configured DICOM SCP receiver definitions.", notes = "The primary DICOM SCP retrieval function returns a list of all DICOM SCP receivers defined for the current system.", response = DicomSCPInstance.class, responseContainer = "List") + @ApiResponses({@ApiResponse(code = 200, message = "A list of DICOM SCP receiver definitions."), @ApiResponse(code = 500, message = "Unexpected error")}) + @RequestMapping(produces = {"application/json", "application/xml"}, method = RequestMethod.GET) + @ResponseBody + public ResponseEntity<List<DicomSCPInstance>> dicomSCPsGet() { + return new ResponseEntity<>(_manager.getDicomSCPInstances(), HttpStatus.OK); + } + + @ApiOperation(value = "Gets the DICOM SCP receiver definition with the specified ID.", notes = "Returns the DICOM SCP receiver definition with the specified ID.", response = DicomSCPInstance.class) + @ApiResponses({@ApiResponse(code = 200, message = "DICOM SCP receiver definition successfully retrieved."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to view this DICOM SCP receiver definition."), @ApiResponse(code = 404, message = "DICOM SCP receiver definition not found."), @ApiResponse(code = 500, message = "Unexpected error")}) + @RequestMapping(value = {"/{id}"}, produces = {"application/json", "application/xml", "text/html"}, method = {RequestMethod.GET}) + public ResponseEntity<DicomSCPInstance> dicomSCPInstanceGet(@ApiParam(value = "ID of the DICOM SCP receiver definition to fetch", required = true) @PathVariable("id") final String id) { + HttpStatus status = isPermitted(id); + if (status != null) { + return new ResponseEntity<>(status); + } + return _manager.hasDicomSCP(id) ? new ResponseEntity<>(_manager.getDicomSCPInstance(id), HttpStatus.OK) + : new ResponseEntity<DicomSCPInstance>(HttpStatus.NOT_FOUND); + } + + @ApiOperation(value = "Creates or updates the DICOM SCP receiver definition object with the specified ID.", notes = "Returns the updated DICOM SCP receiver definition.", response = DicomSCPInstance.class) + @ApiResponses({@ApiResponse(code = 200, message = "DICOM SCP receiver definition successfully created or updated."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to create or update this DICOM SCP receiver definition."), @ApiResponse(code = 404, message = "DICOM SCP receiver definition not found."), @ApiResponse(code = 500, message = "Unexpected error")}) + @RequestMapping(value = {"/{id}"}, produces = {"application/json", "application/xml", "text/html"}, method = {RequestMethod.PUT}) + public ResponseEntity<DicomSCPInstance> dicomSCPInstanceCreateOrUpdate(@ApiParam(value = "The ID of the DICOM SCP receiver definition to create or update.", required = true) @PathVariable("id") final String id, @RequestBody final DicomSCPInstance instance) throws NotFoundException { + HttpStatus status = isPermitted(id); + if (status != null) { + return new ResponseEntity<>(status); + } + if (!_manager.hasDicomSCP(id)) { + try { + _manager.create(instance); + } catch (NrgServiceException e) { + _log.error("An error occurred trying to create a new DICOM SCP instance", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } else { + _manager.setDicomSCPInstance(instance); + } + return new ResponseEntity<>(instance, HttpStatus.OK); + } + + @ApiOperation(value = "Deletes the DICOM SCP receiver definition object with the specified ID.", notes = "This call will stop the receiver if it's currently running.", response = Void.class) + @ApiResponses({@ApiResponse(code = 200, message = "DICOM SCP receiver definition successfully created or updated."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to delete this DICOM SCP receiver definition."), @ApiResponse(code = 404, message = "DICOM SCP receiver definition not found."), @ApiResponse(code = 500, message = "Unexpected error")}) + @RequestMapping(value = {"/{id}"}, produces = {"application/json", "application/xml", "text/html"}, method = {RequestMethod.DELETE}) + public ResponseEntity<Void> dicomSCPInstanceDelete(@ApiParam(value = "The ID of the DICOM SCP receiver definition to delete.", required = true) @PathVariable("id") final String id) throws NotFoundException { + HttpStatus status = isPermitted(id); + if (status != null) { + return new ResponseEntity<>(status); + } + if (!_manager.hasDicomSCP(id)) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + try { + _manager.delete(id); + return new ResponseEntity<>(HttpStatus.OK); + } catch (NrgServiceException e) { + _log.error("An error occurred trying to delete the DICOM SCP instance " + id, e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @ApiOperation(value = "Returns whether the DICOM SCP receiver definition with the specified ID is enabled.", notes = "Returns true or false based on whether the specified DICOM SCP receiver definition is enabled or not.", response = Boolean.class) + @ApiResponses({@ApiResponse(code = 200, message = "DICOM SCP receiver definition enabled status successfully retrieved."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to view this DICOM SCP receiver definition."), @ApiResponse(code = 404, message = "DICOM SCP receiver definition not found."), @ApiResponse(code = 500, message = "Unexpected error")}) + @RequestMapping(value = {"/{id}/enabled"}, produces = {"application/json"}, method = {RequestMethod.GET}) + public ResponseEntity<Boolean> dicomSCPInstanceEnabledGet(@ApiParam(value = "The ID of the DICOM SCP receiver definition to retrieve the enabled status for.", required = true) @PathVariable("id") final String id) { + HttpStatus status = isPermitted(id); + if (status != null) { + return new ResponseEntity<>(status); + } + return _manager.hasDicomSCP(id) ? new ResponseEntity<>(_manager.getDicomSCPInstance(id).isEnabled(), HttpStatus.OK) + : new ResponseEntity<Boolean>(HttpStatus.NOT_FOUND); + } + + @ApiOperation(value = "Sets the DICOM SCP receiver definition's enabled state.", notes = "Sets the enabled state of the DICOM SCP receiver definition with the specified ID to the value of the flag parameter.", response = Void.class) + @ApiResponses({@ApiResponse(code = 200, message = "DICOM SCP receiver definition enabled status successfully set."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to enable or disable this DICOM SCP receiver definition."), @ApiResponse(code = 404, message = "DICOM SCP receiver definition not found."), @ApiResponse(code = 500, message = "Unexpected error")}) + @RequestMapping(value = {"/{id}/enabled/{flag}"}, produces = {"application/json"}, method = {RequestMethod.PUT}) + public ResponseEntity<Void> dicomSCPInstanceSetEnabledFlag(@ApiParam(value = "ID of the DICOM SCP receiver definition to modify", required = true) @PathVariable("id") final String id, + @ApiParam(value = "The value to set for the enabled status.", required = true) @PathVariable("flag") final Boolean flag) { + HttpStatus status = isPermitted(id); + if (status != null) { + return new ResponseEntity<>(status); + } + if (!_manager.hasDicomSCP(id)) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + _manager.getDicomSCPInstance(id).setEnabled(flag); + return new ResponseEntity<>(HttpStatus.OK); + } + + @ApiOperation(value = "Starts all enabled DICOM SCP receivers.", notes = "This starts all enabled DICOM SCP receivers. The return value notes the number of ", responseContainer = "List", response = String.class) + @ApiResponses({@ApiResponse(code = 200, message = "DICOM SCP receivers successfully started."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to enable or disable this DICOM SCP receiver definition."), @ApiResponse(code = 404, message = "DICOM SCP receiver definition not found."), @ApiResponse(code = 500, message = "Unexpected error")}) + @RequestMapping(value = {"/start"}, produces = {"application/json"}, method = {RequestMethod.PUT}) + public ResponseEntity<List<String>> dicomSCPInstancesStart() { + HttpStatus status = isPermitted(); + if (status != null) { + return new ResponseEntity<>(status); + } + return new ResponseEntity<>(_manager.startDicomSCPs(), HttpStatus.OK); + } + + @ApiOperation(value = "Starts the DICOM SCP receiver.", notes = "This starts the DICOM SCP receiver. Note that this will start the receiver regardless of its enabled or disabled setting. This returns true if the instance was started and false if not.", response = Boolean.class) + @ApiResponses({@ApiResponse(code = 200, message = "DICOM SCP receiver successfully started."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to enable or disable this DICOM SCP receiver definition."), @ApiResponse(code = 404, message = "DICOM SCP receiver definition not found."), @ApiResponse(code = 500, message = "Unexpected error")}) + @RequestMapping(value = {"/{id}/start"}, produces = {"application/json"}, method = {RequestMethod.PUT}) + public ResponseEntity<Boolean> dicomSCPInstanceStart(@ApiParam(value = "ID of the DICOM SCP receiver to start.", required = true) @PathVariable("id") final String id) { + HttpStatus status = isPermitted(id); + if (status != null) { + return new ResponseEntity<>(status); + } + if (!_manager.hasDicomSCP(id)) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + _manager.startDicomSCP(id); + return new ResponseEntity<>(true, HttpStatus.OK); + } + + @ApiOperation(value = "Stops all enabled DICOM SCP receivers.", notes = "This stops all enabled DICOM SCP receivers. The return value notes the number of ", responseContainer = "List", response = String.class) + @ApiResponses({@ApiResponse(code = 200, message = "DICOM SCP receivers successfully stopped."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to enable or disable this DICOM SCP receiver definition."), @ApiResponse(code = 404, message = "DICOM SCP receiver definition not found."), @ApiResponse(code = 500, message = "Unexpected error")}) + @RequestMapping(value = {"/stop"}, produces = {"application/json"}, method = {RequestMethod.PUT}) + public ResponseEntity<List<String>> dicomSCPInstancesStop() { + HttpStatus status = isPermitted(); + if (status != null) { + return new ResponseEntity<>(status); + } + return new ResponseEntity<>(_manager.stopDicomSCPs(), HttpStatus.OK); + } + + @ApiOperation(value = "Stops the DICOM SCP receiver.", notes = "This stops the DICOM SCP receiver. Note that this will stop the receiver regardless of its enabled or disabled setting. This returns true if the instance was stoped and false if not.", response = Boolean.class) + @ApiResponses({@ApiResponse(code = 200, message = "DICOM SCP receiver successfully stoped."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to enable or disable this DICOM SCP receiver definition."), @ApiResponse(code = 404, message = "DICOM SCP receiver definition not found."), @ApiResponse(code = 500, message = "Unexpected error")}) + @RequestMapping(value = {"/{id}/stop"}, produces = {"application/json"}, method = {RequestMethod.PUT}) + public ResponseEntity<Boolean> dicomSCPInstanceStop(@ApiParam(value = "ID of the DICOM SCP receiver to stop.", required = true) @PathVariable("id") final String id) { + HttpStatus status = isPermitted(id); + if (status != null) { + return new ResponseEntity<>(status); + } + if (!_manager.hasDicomSCP(id)) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + _manager.stopDicomSCP(id); + return new ResponseEntity<>(true, HttpStatus.OK); + } + + @Inject + private DicomSCPManager _manager; +} diff --git a/src/main/java/org/nrg/xapi/rest/users/UsersApi.java b/src/main/java/org/nrg/xapi/rest/users/UsersApi.java index feb7cd73f1751b4a6b659a92da5f7558f11f02dd..34fe4503d523444757a45266d1664b4310bfe703 100644 --- a/src/main/java/org/nrg/xapi/rest/users/UsersApi.java +++ b/src/main/java/org/nrg/xapi/rest/users/UsersApi.java @@ -3,6 +3,7 @@ package org.nrg.xapi.rest.users; import io.swagger.annotations.*; import org.apache.commons.lang3.StringUtils; import org.nrg.xapi.model.users.User; +import org.nrg.xapi.rest.AbstractXnatRestApi; import org.nrg.xapi.rest.NotFoundException; import org.nrg.xdat.security.XDATUser; import org.nrg.xdat.security.helpers.Users; @@ -24,7 +25,7 @@ import java.util.List; @Api(description = "The XNAT POC User Management API") @RestController @RequestMapping(value = "/users") -public class UsersApi { +public class UsersApi extends AbstractXnatRestApi { private static final Logger _log = LoggerFactory.getLogger(UsersApi.class); @ApiOperation(value = "Get list of users.", notes = "The primary users function returns a list of all users of the XNAT system.", response = User.class, responseContainer = "List") @@ -198,34 +199,6 @@ public class UsersApi { } } - private UserI getSessionUser() { - Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - if ((principal instanceof UserI)) { - return (UserI) principal; - } - return null; - } - - private HttpStatus isPermitted(String id) { - UserI sessionUser = getSessionUser(); - if (sessionUser == null) { - return HttpStatus.UNAUTHORIZED; - } - if ((sessionUser.getUsername().equals(id)) || (isPermitted() == null)) { - return null; - } - return HttpStatus.FORBIDDEN; - } - - private HttpStatus isPermitted() { - UserI sessionUser = getSessionUser(); - if ((sessionUser instanceof XDATUser)) { - return ((XDATUser) sessionUser).isSiteAdmin() ? null : HttpStatus.FORBIDDEN; - } - - return null; - } - @SuppressWarnings("unused") public static class Event { public static String Added = "Added User"; diff --git a/src/main/java/org/nrg/xnat/configuration/DatabaseConfig.java b/src/main/java/org/nrg/xnat/configuration/DatabaseConfig.java index 6fe2217cba7ecb1777d277aadc1cc97c5a252d05..8fb7d5a1b0c11ff86d4be07dd694abba036c544b 100644 --- a/src/main/java/org/nrg/xnat/configuration/DatabaseConfig.java +++ b/src/main/java/org/nrg/xnat/configuration/DatabaseConfig.java @@ -1,40 +1,99 @@ package org.nrg.xnat.configuration; -import org.apache.commons.dbcp.BasicDataSource; -import org.springframework.beans.factory.annotation.Value; +import org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory; +import org.hibernate.cache.spi.RegionFactory; +import org.hibernate.cfg.ImprovedNamingStrategy; +import org.nrg.framework.exceptions.NrgServiceError; +import org.nrg.framework.exceptions.NrgServiceException; +import org.nrg.framework.orm.hibernate.AggregatedAnnotationSessionFactoryBean; +import org.nrg.framework.orm.hibernate.PrefixedTableNamingStrategy; +import org.nrg.framework.utilities.Beans; +import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.SimpleDriverDataSource; +import org.springframework.orm.hibernate4.HibernateTransactionManager; +import org.springframework.orm.hibernate4.LocalSessionFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import javax.inject.Inject; import javax.sql.DataSource; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.Properties; /** * Sets up the database configuration for XNAT. */ @Configuration +@EnableTransactionManagement(proxyTargetClass = true) public class DatabaseConfig { - @Bean - public DataSource dataSource() { - final BasicDataSource dataSource = new BasicDataSource(); - dataSource.setDriverClassName(_driver); - dataSource.setUrl(_url); - dataSource.setUsername(_username); - dataSource.setPassword(_password); - return dataSource; + public DataSource dataSource() throws NrgServiceException { + final Properties properties = Beans.getNamespacedProperties(_environment, "datasource", true); + final String dataSourceClassName = properties.getProperty("class", SimpleDriverDataSource.class.getName()); + try { + final Class<? extends DataSource> clazz = Class.forName(dataSourceClassName).asSubclass(DataSource.class); + if (properties.containsKey("driver")) { + final String driver = (String) properties.get("driver"); + properties.put("driver", Class.forName(driver).newInstance()); + } + return Beans.getInitializedBean(properties, clazz); + } catch (ClassNotFoundException e) { + throw new NrgServiceException(NrgServiceError.ConfigurationError, "Couldn't find the specified data-source class name: " + dataSourceClassName); + } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { + throw new NrgServiceException(NrgServiceError.ConfigurationError, "An error occurred trying to access a property in the specified data-source class: " + dataSourceClassName, e); + } } @Bean - public JdbcTemplate jdbcTemplate() { + public JdbcTemplate jdbcTemplate() throws NrgServiceException { return new JdbcTemplate(dataSource()); } - @Value("${datasource.driver}") - private String _driver; - @Value("${datasource.url}") - private String _url; - @Value("${datasource.username}") - private String _username; - @Value("${datasource.password}") - private String _password; + @Bean + public ImprovedNamingStrategy namingStrategy() { + return new PrefixedTableNamingStrategy("xhbm"); + } + + @Bean + public PropertiesFactoryBean hibernateProperties() { + final PropertiesFactoryBean bean = new PropertiesFactoryBean(); + bean.setProperties(Beans.getNamespacedProperties(_environment, "hibernate", false)); + return bean; + } + + @Bean + public RegionFactory regionFactory() throws NrgServiceException { + try { + return new SingletonEhCacheRegionFactory(hibernateProperties().getObject()); + } catch (IOException e) { + throw new NrgServiceException(NrgServiceError.Unknown, "An error occurred trying to retrieve the Hibernate properties", e); + } + } + + @Bean + public LocalSessionFactoryBean sessionFactory() throws NrgServiceException { + try { + final AggregatedAnnotationSessionFactoryBean bean = new AggregatedAnnotationSessionFactoryBean(); + bean.setDataSource(dataSource()); + bean.setCacheRegionFactory(regionFactory()); + bean.setHibernateProperties(hibernateProperties().getObject()); + bean.setNamingStrategy(namingStrategy()); + return bean; + } catch (IOException e) { + throw new NrgServiceException(NrgServiceError.Unknown, "An error occurred trying to retrieve the Hibernate properties", e); + } + } + + @Bean + public PlatformTransactionManager transactionManager() throws NrgServiceException { + return new HibernateTransactionManager(sessionFactory().getObject()); + } + + @Inject + private Environment _environment; } diff --git a/src/main/java/org/nrg/xnat/configuration/DicomImportConfig.java b/src/main/java/org/nrg/xnat/configuration/DicomImportConfig.java index 35164494b10c42d83973b895e8d1444b86f29da5..62cda997c72b5c8506a3156b908b29bb98a85263 100644 --- a/src/main/java/org/nrg/xnat/configuration/DicomImportConfig.java +++ b/src/main/java/org/nrg/xnat/configuration/DicomImportConfig.java @@ -31,8 +31,8 @@ public class DicomImportConfig { } @Bean - public DicomSCPManager dicomSCPManager(final XnatUserProvider provider) throws Exception { - return new DicomSCPManager(provider); + public DicomSCPManager dicomSCPManager() throws Exception { + return new DicomSCPManager(); } @Bean diff --git a/src/main/java/org/nrg/xnat/configuration/PropertiesConfig.java b/src/main/java/org/nrg/xnat/configuration/PropertiesConfig.java index 4e54061a2e675b1d62ac93541484d8a70b486c45..b2e133d440606f1ab92e082f5ad726d9841a9f9d 100644 --- a/src/main/java/org/nrg/xnat/configuration/PropertiesConfig.java +++ b/src/main/java/org/nrg/xnat/configuration/PropertiesConfig.java @@ -25,9 +25,6 @@ import java.util.List; */ @Configuration @PropertySources({ - @PropertySource(value = "file:${HOME}/config/services.properties", ignoreResourceNotFound = true), - @PropertySource(value = "file:${HOME}/xnat/config/services.properties", ignoreResourceNotFound = true), - @PropertySource(value = "file:${XNAT_HOME}/config/services.properties", ignoreResourceNotFound = true), @PropertySource(value = "file:${xnat.home}/config/services.properties", ignoreResourceNotFound = true), @PropertySource(value = "file:${xnat.config.home}/services.properties", ignoreResourceNotFound = true), @PropertySource(value = "file:${xnat.config}", ignoreResourceNotFound = true)}) diff --git a/src/main/java/org/nrg/xnat/initialization/RootConfig.java b/src/main/java/org/nrg/xnat/initialization/RootConfig.java index e00af60cadb2d0b2c1a06a006ed0eeae76ede0ec..6ae818e422bad777c60226fdf8bef428a53179b9 100644 --- a/src/main/java/org/nrg/xnat/initialization/RootConfig.java +++ b/src/main/java/org/nrg/xnat/initialization/RootConfig.java @@ -36,11 +36,11 @@ import java.util.List; "org.nrg.prefs.repositories", "org.nrg.prefs.services.impl.hibernate", "org.nrg.dicomtools.filters"}) -@ImportResource({"WEB-INF/conf/xnat-security.xml", "WEB-INF/conf/orm-config.xml", "WEB-INF/conf/mq-context.xml"}) +@ImportResource({"WEB-INF/conf/xnat-security.xml", "WEB-INF/conf/mq-context.xml"}) public class RootConfig { - public static final List<String> DEFAULT_ENTITY_PACKAGES = Arrays.asList("org.nrg.framework.datacache", "org.nrg.xft.entities", "org.nrg.xdat.entities", - "org.nrg.xnat.entities", "org.nrg.prefs.entities", "org.nrg.config.entities"); + private static final List<String> DEFAULT_ENTITY_PACKAGES = Arrays.asList("org.nrg.framework.datacache", "org.nrg.xft.entities", "org.nrg.xdat.entities", + "org.nrg.xnat.entities", "org.nrg.prefs.entities", "org.nrg.config.entities"); @Bean public String siteId() { diff --git a/src/main/java/org/nrg/xnat/initialization/XnatWebAppInitializer.java b/src/main/java/org/nrg/xnat/initialization/XnatWebAppInitializer.java index 5d89b0b30b889970285f0e1ce3168fc61dcd5c31..55ca5b3e349e21300aa2e6c15056b69d28ec8518 100644 --- a/src/main/java/org/nrg/xnat/initialization/XnatWebAppInitializer.java +++ b/src/main/java/org/nrg/xnat/initialization/XnatWebAppInitializer.java @@ -6,7 +6,7 @@ import org.apache.axis.transport.http.AxisServlet; import org.apache.commons.lang3.StringUtils; import org.apache.turbine.Turbine; import org.nrg.framework.exceptions.NrgServiceRuntimeException; -import org.nrg.framework.processors.XnatModuleBean; +import org.nrg.framework.processors.XnatPluginBean; import org.nrg.xdat.servlet.XDATAjaxServlet; import org.nrg.xdat.servlet.XDATServlet; import org.nrg.xnat.restlet.servlet.XNATRestletServlet; @@ -62,14 +62,14 @@ public class XnatWebAppInitializer extends AbstractAnnotationConfigDispatcherSer @Override protected String[] getServletMappings() { - return new String[] { "/admin/*", "/xapi/*" }; + return new String[] {"/admin/*", "/xapi/*"}; } @Override protected Class<?>[] getRootConfigClasses() { final List<Class<?>> configClasses = new ArrayList<>(); configClasses.add(RootConfig.class); - configClasses.addAll(getModuleConfigs()); + configClasses.addAll(getPluginConfigs()); return configClasses.toArray(new Class[configClasses.size()]); } @@ -101,7 +101,7 @@ public class XnatWebAppInitializer extends AbstractAnnotationConfigDispatcherSer tmpDir.toFile().deleteOnExit(); return new MultipartConfigElement(tmpDir.toAbsolutePath().toString(), MAX_FILE_SIZE, MAX_REQUEST_SIZE, FILE_SIZE_THRESHOLD); } catch (IOException e) { - throw new NrgServiceRuntimeException("An error occurred trying to create the temp folder " + prefix + " in the containing folder "+ root); + throw new NrgServiceRuntimeException("An error occurred trying to create the temp folder " + prefix + " in the containing folder " + root); } } @@ -111,35 +111,35 @@ public class XnatWebAppInitializer extends AbstractAnnotationConfigDispatcherSer private static final int FILE_SIZE_THRESHOLD = 0; // Threshold turned off. - private List<Class<?>> getModuleConfigs() { - final List<Class<?>> moduleConfigs = new ArrayList<>(); + private List<Class<?>> getPluginConfigs() { + final List<Class<?>> configs = new ArrayList<>(); try { final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); - final Resource[] resources = resolver.getResources("classpath*:META-INF/xnat/**/*-module.properties"); + final Resource[] resources = resolver.getResources("classpath*:META-INF/xnat/**/*-plugin.properties"); for (final Resource resource : resources) { - final Properties properties = PropertiesLoaderUtils.loadProperties(resource); - final XnatModuleBean module = new XnatModuleBean(properties); - final Class<?> moduleConfig = module.getConfigClass(); - moduleConfigs.add(moduleConfig); + final Properties properties = PropertiesLoaderUtils.loadProperties(resource); + final XnatPluginBean plugin = new XnatPluginBean(properties); + final Class<?> config = plugin.getConfigClass(); + configs.add(config); } } catch (IOException e) { - throw new RuntimeException("An error occurred trying to locate XNAT module definitions."); + throw new RuntimeException("An error occurred trying to locate XNAT plugin definitions."); } catch (ClassNotFoundException e) { - _log.error("Did not find a class specified in a module definition.", e); + _log.error("Did not find a class specified in a plugin definition.", e); } - return moduleConfigs; + return configs; } private void addServlet(final Class<? extends Servlet> clazz, final int loadOnStartup, final String... mappings) { - final String name = StringUtils.uncapitalize(clazz.getSimpleName()); - final ServletRegistration.Dynamic registration = _context.addServlet(name, clazz); + final String name = StringUtils.uncapitalize(clazz.getSimpleName()); + final ServletRegistration.Dynamic registration = _context.addServlet(name, clazz); registration.setLoadOnStartup(loadOnStartup); registration.addMapping(mappings); } private static class XnatTurbineConfig implements ServletConfig { - public XnatTurbineConfig(final ServletContext context) { + XnatTurbineConfig(final ServletContext context) { _context = context; } diff --git a/src/main/java/org/nrg/xnat/restlet/XNATApplication.java b/src/main/java/org/nrg/xnat/restlet/XNATApplication.java index a78986a6e74cc27e69c09aa9effc193367d4f23a..64cd207498146e2b2f62cfd9ab2cbc677f6f90ab 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/extensions/DicomSCPRestlet.java b/src/main/java/org/nrg/xnat/restlet/extensions/DicomSCPRestlet.java index cb3c30356b2d1c24c3211ff4385e96cdf476ff4c..56617d3280cd269620881c0bc8bb3914b4f61906 100644 --- a/src/main/java/org/nrg/xnat/restlet/extensions/DicomSCPRestlet.java +++ b/src/main/java/org/nrg/xnat/restlet/extensions/DicomSCPRestlet.java @@ -151,23 +151,15 @@ public class DicomSCPRestlet extends SecureResource { if (StringUtils.isBlank(_scpId)) { getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST, "You must specify a specific DICOM SCP instance to enable."); } else { - try { - _dicomSCPManager.enableDicomSCP(_scpId); - returnDefaultRepresentation(); - } catch (IOException e) { - getResponse().setStatus(Status.SERVER_ERROR_INTERNAL, e, "An error occurred trying to enable the DICOM SCP instance for ID: " + _scpId); - } + _dicomSCPManager.enableDicomSCP(_scpId); + returnDefaultRepresentation(); } } else if (_action.equalsIgnoreCase("disable")) { if (StringUtils.isBlank(_scpId)) { getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST, "You must specify a specific DICOM SCP instance to disable."); } else { - try { - _dicomSCPManager.disableDicomSCP(_scpId); - returnDefaultRepresentation(); - } catch (IOException e) { - getResponse().setStatus(Status.SERVER_ERROR_INTERNAL, e, "An error occurred while disabling the DICOM receiver " + _scpId); - } + _dicomSCPManager.disableDicomSCP(_scpId); + returnDefaultRepresentation(); } } else { _dicomSCPManager.startDicomSCPs(); 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 bea77c2f44bdb96f504e280e5f9d23931d0a5e8a..6407ba30c303134227511a11a99ca53d64f109ab 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 0000000000000000000000000000000000000000..4d62c1824fc4e387817d95eae1e6bce9da1f23b0 --- /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/WEB-INF/conf/orm-config.xml b/src/main/webapp/WEB-INF/conf/orm-config.xml deleted file mode 100644 index 4bea2ff4e64029bfaef813aca208f481a8ff63af..0000000000000000000000000000000000000000 --- a/src/main/webapp/WEB-INF/conf/orm-config.xml +++ /dev/null @@ -1,47 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - ~ D:/Development/XNAT/1.6/xnat_builder_1_6dev/plugin-resources/conf/orm-config.xml - ~ XNAT http://www.xnat.org - ~ Copyright (c) 2014, Washington University School of Medicine - ~ All Rights Reserved - ~ - ~ Released under the Simplified BSD. - ~ - ~ Last modified 2/7/14 12:19 PM - --> -<beans xmlns="http://www.springframework.org/schema/beans" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:p="http://www.springframework.org/schema/p" - xmlns:tx="http://www.springframework.org/schema/tx" - xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd - http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> - - <!-- Used the prefixed table naming strategy, class FooBar gives you prefix_foo_bar instead of FooBar. --> - <bean id="namingStrategy" class="org.nrg.framework.orm.hibernate.PrefixedTableNamingStrategy" p:prefix="xhbm" /> - - <!-- Set up default hibernate properties. --> - <bean id="hibernateProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean"> - <property name="properties"> - <props> - <prop key="hibernate.dialect">org.hibernate.dialect.PostgreSQL9Dialect</prop> - <prop key="hibernate.hbm2ddl.auto">update</prop> - <prop key="hibernate.show_sql">false</prop> - <prop key="hibernate.cache.use_second_level_cache">true</prop> - <prop key="hibernate.cache.use_query_cache">true</prop> - </props> - </property> - </bean> - - <bean id="regionFactory" class="org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory"> - <constructor-arg index="0" ref="hibernateProperties" /> - </bean> - - <bean id="sessionFactory" class="org.nrg.framework.orm.hibernate.AggregatedAnnotationSessionFactoryBean" - p:cacheRegionFactory-ref="regionFactory" p:dataSource-ref="dataSource" - p:hibernateProperties-ref="hibernateProperties" p:namingStrategy-ref="namingStrategy" /> - - <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager" p:sessionFactory-ref="sessionFactory" /> - - <tx:annotation-driven proxy-target-class="true" transaction-manager="transactionManager"/> - -</beans> diff --git a/src/main/webapp/WEB-INF/conf/services.properties b/src/main/webapp/WEB-INF/conf/services.properties index 17081f1e259c05cd75b9755771a3583cc46d0e9d..de1d5c7fac4e77dbee428cf9994d40c80eea32e3 100644 --- a/src/main/webapp/WEB-INF/conf/services.properties +++ b/src/main/webapp/WEB-INF/conf/services.properties @@ -16,6 +16,12 @@ datasource.url=jdbc:postgresql://localhost/xnat datasource.username=xnat datasource.password=xnat +hibernate.dialect=org.hibernate.dialect.PostgreSQL9Dialect +hibernate.hbm2ddl.auto=update +hibernate.show_sql=false +hibernate.cache.use_second_level_cache=true +hibernate.cache.use_query_cache=true + mailserver.host=mail.server mailserver.port=25 mailserver.username= diff --git a/src/main/webapp/scripts/lib/jquery-plugins/jquery.dataAttr.js b/src/main/webapp/scripts/lib/jquery-plugins/jquery.dataAttr.js new file mode 100644 index 0000000000000000000000000000000000000000..c002e2878696ae007d9d77d864d4158633a64326 --- /dev/null +++ b/src/main/webapp/scripts/lib/jquery-plugins/jquery.dataAttr.js @@ -0,0 +1,202 @@ +/*! + * Get or set [data-] attributes and + * keep jQuery's .data object in sync + * with those attributes with special + * handling for non-string data and + * proper retrieval of functions stored + * in jQuery's .data object. + */ + + +/** + * Sets value for name of [data-] attribute. Also adds value + * to name prop in jQuery object that .dataAttr() is called on. + * Returns a new object containing properties from the element's + * .dataset property and jQuery .data object. + * jQuery().data().name values override + * element.dataset.name properties. + * @param {string} name - Name of [data-] attribute + * @param {string|array|object|function} value - The value to assign to name + * @return {object} + * Returns object map of data attributes and values + * OR the jQuery object .dataAttr() was called on + */ +$.fn.dataAttr = function(name, value){ + + var $this = this, + el = this[0], + attrVal = value, + fnName; + + if (!arguments.length){ + // return all [data-] attrs merged + // with jQuery's .data object + // if no args are passed + return $.extend(true, {}, el.dataset, $this.data()); + } + + function processAttrs(){ + // future function to consolidate + // .dataAttr and .dataAttrs + // into one function + } + + if ($.isPlainObject(name) || $.isArray(name)){ + return $this.dataAttrs(name); + } + + // TODO: separate vars - attrName, dataName, attrVal, dataVal + + // strip data prefix from name + name = name.replace(/^(data\-+|data)/,''); + + // ensure [data-] attribute names are hy-phen-at-ed + // and data: {} property names are camelCased + var dataAttrName = 'data-' + toDashed(name), + dataPropName = toCamelCase(name), + // events start with 'on' + isEvent = dataPropName.indexOf('on') === 0, + // event name is the [data-] attribute name WITHOUT 'on' + EVENT_NAME = isEvent ? dataPropName.toLowerCase().replace(/^on/,'') : null; + + if (arguments.length === 1) { + // get the value from the element attribute + // instead of the data object??? + if ($.isFunction($this.data(dataPropName))){ + // unless it's a function + return function(/* arguments */){ + $this.data(dataPropName).apply(el, arguments); + return $this; + } + } + try { + return JSON.parse($this.attr(dataAttrName)); + } + catch(e){ + //console.log(e); + try { + return $this.attr(dataAttrName); + } + catch(ee){ + console.log(ee); + } + } + } + else if (value === null) { + // remove jQuery data and element's [data-] attribute + // if value is null + $this.removeData(dataPropName).removeAttr(dataAttrName); + if (EVENT_NAME){ + $this.off(EVENT_NAME); + } + } + else { + // if it's a function, it will be available via jQuery's .data() method + if ($.isFunction(attrVal)) { + //fnName = "function(){return $('" + $this.selector + "').dataAttr('" + dataPropName + "'))"; + fnName = "$('" + $this.selector + "').dataAttr('" + dataPropName + "')"; + // (escape quotes?) and store the function name + //attrVal = fnName.replace(/'/g,"\\'").replace(/"/g,'\\"'); + attrVal = fnName; + //attrVal = "fn:"+dataPropName; // store the function name + //attrVal = '()'; + //attrVal = 'function:' + dataPropName; + if (console && console.debug){ + console.debug( + 'To invoke the function stored in jQuery\'s .data ' + + 'object for this element, call ' + fnName + '();' + ); + } + // then to call it, just do: + // var func = $('#el').data('foo'); + // func(); + } + attrVal = (typeof attrVal === 'string') ? attrVal : JSON.stringify(attrVal); + $this.data(dataPropName, value).attr(dataAttrName, attrVal); + if (EVENT_NAME){ + $(document.body).on(EVENT_NAME, $this.selector, $this.data(dataPropName)); + } + } + + function toCamelCase(str){ + return str.toLowerCase().replace(/\-./g, function(u){ + return u.substr(1).toUpperCase(); + }); + } + + function toDashed(str){ + return str.replace(/([A-Z])/g, function(u){ + return '-' + u; + }).toLowerCase(); + } + + // return the jQuery object for chaining + return $this; + +}; + +// handle multiple [data-] attributes +// as an object map +// property names must be camelCase 'attrName' +// and will map to hyphenated 'data-attr-name' +// (do NOT include 'data' in property names) +$.fn.dataAttrs = function(obj){ + + var $this = this, + name, val, + datas = {}, + i = -1; + + if (!arguments.length){ + return $this.dataAttr(); + } + + if ($.isPlainObject(obj)){ + for (name in obj){ + if (obj.hasOwnProperty(name)){ + val = obj[name]; + $this.dataAttr(name, val); + } + } + } + // return an object map of name: value + // if an array of [data-] attribute names + // is passed + else if ($.isArray(obj)){ + while (++i < obj.length){ + name = obj[i]; + datas[name] = $this.dataAttr(name); + } + return datas; + } + else { + return $this.dataAttr.apply($this, arguments); + } + + return $this; + +}; + +// remove [data-] attributes and jQuery .data() properties +// pass multiple names or array of names as argument(s) +// to remove more than one [data-] attribute +$.fn.removeDataAttrs = $.fn.removeDataAttr = function(/* name(s) */){ + + var $this = this, + names = [].concat(arguments[0]||null), + i = -1, + name; + + if (arguments.length > 1){ + names = $.makeArray(arguments); + } + + while (++i < names.length){ + name = names[i]; + if (name && typeof name == 'string'){ + $this.dataAttr(name, null); + } + } + + return $this; +}; diff --git a/src/main/webapp/scripts/lib/spawn/spawn.js b/src/main/webapp/scripts/lib/spawn/spawn.js index fc402ca87e106ee6c2186e81e9276e7dad8f00c9..31b528986a4eecd3a7d2f4fca0cfa106c7266c9a 100644 --- a/src/main/webapp/scripts/lib/spawn/spawn.js +++ b/src/main/webapp/scripts/lib/spawn/spawn.js @@ -11,7 +11,8 @@ (function(window, doc){ var undefined, - UNDEFINED = 'undefined'; + UNDEFINED = 'undefined', + hasConsole = console && console.log; // which HTML elements are // self-closing "void" elements? @@ -55,6 +56,444 @@ 'hidden' ]; + // use these as a shortcut to create <input> elements: + // spawn.html('input|text') + // + var inputTags = inputTypes.map(function(type){ + return 'input|' + type; + }); + + + function parseAttrs(el, attrs){ + // allow ';' or ',' for attribute delimeter + (attrs.split(/;|,/) || []).forEach(function(att, i){ + if (!att) return; + // allow ':' or '=' for key/value separator + var sep = /:|=/; + // tolerate quotes around values + var quotes = /^['"]+|['"]+$/g; + var key = att.split(sep)[0].trim(); + var val = (att.split(sep)[1]||'').trim().replace(quotes, '') || key; + // allow use of 'class', but (secretly) use 'className' + if (key === 'class') { + el.className = val; + return; + } + el.setAttribute(key, val); + }); + } + + + function appendChildren(el, children, fn){ + [].concat(children).forEach(function(child){ + // each 'child' can be an array of + // spawn arrays... + if (Array.isArray(child)){ + el.appendChild(fn.apply(el, child)); + } + // ...or an HTML string (or number)... + else if (/(string|number)/.test(typeof child)){ + el.innerHTML += child; + } + // ...or 'appendable' nodes + else { + el.appendChild(child); + } + }); + } + + + /** + * Fairly lean and fast element spawner that's + * a little more robust than spawn.element(). + * @param tag {String} element's tagName + * @param [opts] {Object|Array|String} element + * properties/attributes -or- array of + * children -or- HTML string + * @param [children] {Array|String} + * array of child element 'spawn' arg arrays + * or elements or HTML string + * @returns {Element|*} + */ + function spawn(tag, opts, children){ + + var el, $el, parts, id, classes, tagParts, attrs, isVoid, + skip = ['innerHTML', 'html', 'append', // properties to skip later + 'classes', 'attr', 'data', 'fn'], + errors = []; // collect errors + + if (tag === '!'){ + el = doc.createDocumentFragment(); + appendChildren(el, opts, spawn); + return el; + } + + try { + parts = tag.split('|'); + } + catch(e){ + if (hasConsole) console.log(e); + parts = ['div.bogus']; + } + + tag = parts.shift().trim(); + + // also allow 'tag' to use selector syntax for id and class + // spawn.plus('div#foo.bar', 'Foo'); + // -> '<div id="foo" class="bar">Foo</div>' + // split classes first + classes = tag.split('.'); + + // first item will ALWAYS be tag name + // wich MAY also have an id + tag = classes.shift(); + + tagParts = tag.split('#'); + + tag = tagParts.shift(); + + isVoid = voidElements.indexOf(tag) > -1; + + el = doc.createElement(tag||'div'); + + if (tagParts.length){ + id = tagParts[0]; + } + + if (id){ + el.id = id; + } + + if (classes.length){ + el.className = classes.join(' ').trim(); + } + + if (parts.length){ + // pass element attributes in 'tag' string, like: + // spawn('a|id="foo-link";href="foo";class="bar"'); + // or (colons for separators, commas for delimeters, no quotes),: + // spawn('input|type:checkbox,id:foo-ckbx'); + parseAttrs(el, parts[0]||''); + } + + if (!opts && !children){ + // return early for + // basic element creation + return el; + } + + opts = opts || {}; + children = children || null; + + if (!isVoid){ + // if 'opts' is a string, + // set el's innerHTML and + // return the element + if (/(string|number)/.test(typeof opts)){ + el.innerHTML += opts; + return el; + } + + // if 'children' arg is not present + // and 'opts' is really an array + if (!children && Array.isArray(opts)){ + children = opts; + opts = {}; + } + // or if 'children' is a string + // set THAT to the innerHTML + else if (/(string|number)/.test(typeof children)){ + el.innerHTML += children; + children = null; + } + } + + // allow use of 'classes' property for classNames + if (opts.className || opts.classes){ + el.className = [].concat(opts.className||[], opts.classes||[]).join(' ').trim(); + } + + // add attributes and properties to element + forOwn(opts, function(prop, val){ + // only add if NOT in 'skip' array + if (skip.indexOf(prop) === -1){ + el[prop] = val; + } + }); + + // explicitly add element attributes + if (opts.attr){ + forOwn(opts.attr, function(name, val){ + el.setAttribute(name, val); + }); + } + + // explicitly add 'data-' attributes + if (opts.data){ + forOwn(opts.data, function(name, val){ + setElementData(el, name, [].concat(val).join('')); + }); + } + + // only add innerHTML and children for non-void elements + if (!isVoid){ + + // special handling of 'prepend' property + if (opts.prepend){ + try { + appendChildren(el, opts.prepend, spawn) + } + catch(e){ + // write error to console + if (hasConsole) console.log(e); + //errors.push('Error appending: ' + e); + } + } + + // add innerHTML now, if present + el.innerHTML += (opts.innerHTML||opts.html||''); + + // append any spawned children + if (children){ + try { + appendChildren(el, children, spawn) + } + catch(e){ + // write error to console + if (hasConsole) console.log(e); + //errors.push('Error appending: ' + e); + } + } + + // special handling of 'append' property + if (opts.append){ + try { + appendChildren(el, opts.append, spawn) + } + catch(e){ + // write error to console + if (hasConsole) console.log(e); + //errors.push('Error appending: ' + e); + } + } + } + + // execute element methods last... + // attach object or array of methods + // to 'fn' property - this can be an + // array in case you want to run the + // same method(s) more than once + if (opts.fn){ + [].concat(opts.fn).forEach(function(fn){ + forOwn(fn, function(f, args){ + el[f].apply(el, [].concat(args)); + }); + }); + } + + // execute jQuery methods from the `$` property + if (opts.$ && window.$){ + $el = $(el); + forOwn(opts.$, function(method, args){ + $el[method].apply($el, [].concat(args)); + }); + } + + if (errors.length){ + if (hasConsole) console.log(errors); + } + + return el; + + } + // aliases + spawn.plus = spawn; + spawn.lite = spawn; + spawn.alt = spawn; + + /** + * Leanest and fastest element spawner + * @param tag {String|Object} tag name or config object + * @param [opts] {Object|String|Array} config object, HTML content, or array of Elements + * @param [content] {String|Array} HTML content or array of Elements + * @returns {Element|*} + */ + spawn.element = function spawnElement(tag, opts, content){ + + var el, $el, id, classes, tagParts; + + if (typeof tag != 'string'){ + // if 'tag' isn't a string, + // it MUST be a config object + opts = tag; + // and it MUST have a 'tag' + // or 'tagName' property + tag = opts.tag || opts.tagName || 'div'; + } + + // also allow 'tag' to use selector syntax for id and class + // spawn.plus('div#foo.bar', 'Foo'); + // -> '<div id="foo" class="bar">Foo</div>' + // split classes first + classes = tag.split('.'); + + // first item will ALWAYS be tag name + // wich MAY also have an id + tag = classes.shift(); + + tagParts = tag.split('#'); + + tag = tagParts[0]; + + el = doc.createElement(tag||'div'); + + if (tagParts.length > 1){ + id = tagParts[1]; + } + + if (id){ + el.id = id; + } + + if (classes.length){ + el.className = classes.join(' ').trim(); + } + + // return early for basic usage + if (!content && !opts && typeof tag == 'string') { + return el; + } + + // allow use of only 2 arguments + // with the HTML text being the second + if (/(string|number)/.test(typeof opts)){ + el.innerHTML += (opts+''); + return el; + } + else if (Array.isArray(opts)){ + content = opts; + opts = {}; + } + + if (opts.classes || opts.className){ + el.className = [].concat(el.className||[], opts.classes||[], opts.className||[]).join(' '); + } + + if (opts.html){ + opts.innerHTML += opts.html+''; + } + + // add attributes and properties to element + forOwn(opts, function(prop, val){ + if (/^(tag|html|classes|attr|data|$)$/.test(prop)) return; + el[prop] = val; + }); + + // explicitly add element attributes + if (opts.attr){ + forOwn(opts.attr, function(name, val){ + el.setAttribute(name, val); + }); + } + + // explicitly add 'data-' attributes + if (opts.data){ + forOwn(opts.data, function(name, val){ + setElementData(el, name, [].concat(val).join('')); + }); + } + + // add any HTML content or child elements + if (content){ + appendChildren(el, content, spawnElement); + } + + if (opts.$){ + $el = $(el); + forOwn(opts.$, function(method, args){ + $el[method].apply($el, [].concat(args)); + }); + //delete opts.$; + } + + return el; + + }; + + /** + * Spawn an HTML string using input parameters + * Simple and fast but only generates HTML + * @param tag {String} tag name for HTML element + * @param [attrs] {Object|Array|String} element attributes + * @param [content] {String|Array} string or array of strings for HTML content + * @returns {String} HTML string + */ + spawn.html = function spawnHTML(tag, attrs, content){ + // the 'html' method can be useful + // for easily churning out plain old HTML + // no event handlers or other methods + + tag = tag || 'div'; + attrs = attrs || null; + content = content || []; + + var output = {}; + output.inner = ''; + output.attrs = ''; + + if (inputTags.indexOf(tag) > -1){ + tag = tag.split('|'); + output.attrs += (' type="' + tag[1] +'"'); + tag = tag[0]; + // maybe set 'content' as the value? + output.attrs += (' value="' + content + '"'); + // add content to [data-content] attribute? + //output.attrs += (' data-content="' + content + '"'); + } + + var isVoid = voidElements.indexOf(tag) > -1; + + if (isVoid){ + output.open = '<' + tag; + output.close = '>'; + } + else { + output.open = '<' + tag; + output.inner = '>' + [].concat(content).map(function(child){ + if (Array.isArray(child)){ + return spawnHTML.apply(null, child) + } + else { + return child+'' + } + }).join(''); + output.close = '</' + tag + '>'; + } + + // process the attributes; + if (attrs){ + if (isPlainObject(attrs)){ + forOwn(attrs, function(attr, val){ + if (boolAttrs.indexOf(attr) > -1){ + if (attr){ + // boolean attributes don't need a value + output.attrs += (' ' + attr); + } + } + else { + output.attrs += (' ' + attr + '="' + val + '"'); + } + }); + } + else { + output.attrs += [''].concat(attrs).join(' '); + } + } + + return output.open + output.attrs + output.inner + output.close; + + }; + /** * Full-featured (but slowest) element spawner * @param tag {String|Object} tag name or config object @@ -62,7 +501,7 @@ * @param [inner] {String|Array} HTML string or array of 'appendable' items * @returns {Element|*} */ - function spawn(tag, opts, inner){ + spawn.extreme = spawn.xt = function(tag, opts, inner){ var el, parts, attrs, use$, $el, children, DIV = doc.createElement('div'), @@ -128,7 +567,7 @@ contents = contents(); } catch(e){ - if (console && console.log) console.log(e); + if (hasConsole) console.log(e); contents = []; } } @@ -171,7 +610,7 @@ el = doc.createElement(tag || 'span'); } catch(e) { - if (console && console.log) console.log(e); + if (hasConsole) console.log(e); el = doc.createDocumentFragment(); el.appendChild(doc.createTextNode(tag || '')); } @@ -191,8 +630,11 @@ var quotes = /^('|")|('|")$/g; var key = att.split(sep)[0].trim(); var val = (att.split(sep)[1] || '').trim().replace(quotes, '') || key; - // add each attribute/property directly to DOM element - //el[key] = val; + // allow use of 'class', but (secretly) use 'className' + if (key === 'class') { + el.className = val; + return; + } el.setAttribute(key, val); }); @@ -292,7 +734,7 @@ } } catch(e) { - if (console && console.log) console.log(e); + if (hasConsole) console.log(e); } }); // that's it... 'contents' HAS to be one of the following @@ -306,13 +748,13 @@ forOwn($opts, function(method, args){ // accept on/off event handlers with varying // number of arguments - if (/^(on|off)$/.test(method.toLowerCase())) { + if (/^(on|off)$/i.test(method)) { forOwn(args, function(evt, fn){ try { $el[method].apply($el, [].concat(evt, fn)); } catch(e) { - if (console && console.log) console.log(e); + if (hasConsole) console.log(e); } }); return; @@ -324,317 +766,10 @@ return el; - } - - /** - * Leanest and fastest element spawner - * @param tag {String|Object} tag name or config object - * @param [opts] {Object|String|Array} config object, HTML content, or array of Elements - * @param [content] {String|Array} HTML content or array of Elements - * @returns {Element|*} - */ - spawn.element = function(tag, opts, content){ - - var el; - - if (typeof tag != 'string'){ - // if 'tag' isn't a string, - // it MUST be a config object - opts = tag; - // and it MUST have a 'tag' - // or 'tagName' property - tag = opts.tag || opts.tagName || 'div'; - } - - el = doc.createElement(tag||'div'); - - // return early for basic usage - if (!content && !opts && typeof tag == 'string') { - return el; - } - - // allow use of only 2 arguments - // with the HTML text being the second - if (/(string|number)/.test(typeof opts)){ - el.innerHTML += (opts+''); - return el; - } - else if (Array.isArray(opts)){ - content = opts; - opts = {}; - } - - // add attributes and properties to element - forOwn(opts, function(prop, val){ - if (prop === 'tag') return; - el[prop] = val; - }); - - // add any HTML content or child elements - if (content){ - [].concat(content).forEach(function(item){ - if (/(string|number)/.test(typeof item)){ - el.innerHTML += (item+''); - } - else { - el.appendChild(item); - } - }); - } - - return el; - - }; - // alias - spawn.basic = spawn.element; - - /** - * Fairly lean and fast element spawner that's - * a little more robust than spawn.element(). - * @param tag {String} element's tagName - * @param [opts] {Object|Array|String} element - * properties/attributes -or- array of - * children -or- HTML string - * @param [children] {Array|String} - * array of child element 'spawn' arg arrays - * or elements or HTML string - * @returns {Element} - */ - spawn.plus = function(tag, opts, children){ - - var el, parts, attrs, - skip = [], // properties to skip later - errors = []; // collect errors - - parts = tag.split('|'); - - tag = parts.shift().trim(); - - el = doc.createElement(tag||'div'); - - if (parts.length){ - // pass element attributes in 'tag' string, like: - // spawn('a|id="foo-link";href="foo";class="bar"'); - // or (colons for separators, commas for delimeters, no quotes),: - // spawn('input|type:checkbox,id:foo-ckbx'); - attrs = (parts[0]||'').split(/;|,/) || []; // allow ';' or ',' for attribute delimeter - attrs.forEach(function(att, i){ - if (!att) return; - var sep = /:|=/; // allow ':' or '=' for key/value separator - var quotes = /^('|")|('|")$/g; - var key = att.split(sep)[0].trim(); - var val = (att.split(sep)[1]||'').trim().replace(quotes, '') || key; - // allow use of 'class', but (secretly) use 'className' - if (key === 'class') { - el.className = val; - return; - } - el.setAttribute(key, val); - }); - } - - if (!opts && !children){ - // return early for - // basic element creation - return el; - } - - opts = opts || {}; - children = children || null; - - // if 'opts' is a string, - // set el's innerHTML and - // return the element - if (typeof opts == 'string'){ - el.innerHTML += opts; - return el; - } - - // if 'children' arg is not present - // and 'opts' is really an array - if (!children && Array.isArray(opts)){ - children = opts; - opts = {}; - } - // or if 'children' is a string - // set THAT to the innerHTML - else if (typeof children == 'string'){ - el.innerHTML += children; - children = null; - } - - // add innerHTML now, if present - el.innerHTML += (opts.innerHTML||opts.html||''); - - // append any spawned children - if (children && Array.isArray(children)){ - children.forEach(function(child){ - // each 'child' can be an array of - // spawn arrays... - if (Array.isArray(child)){ - el.appendChild(spawn.plus.apply(el, child)); - } - // ...or an HTML string... - else if (typeof child == 'string'){ - el.innerHTML += child; - } - // ...or 'appendable' nodes - else { - try { - el.appendChild(child); - } - catch(e){ - // fail silently - errors.push('Error processing children: ' + e); - } - } - }); - } - - // special handling of 'append' property - if (opts.append){ - // a string should be HTML - if (typeof opts.append == 'string'){ - el.innerHTML += opts.append; - } - // otherwise an 'appendable' node - else { - try { - el.appendChild(opts.append); - } - catch(e){ - errors.push('Error appending: ' + e); - } - } - } - - // DO NOT ADD THESE DIRECTLY TO 'el' - skip.push('innerHTML', 'html', 'append', 'attr', 'data', 'fn'); - - // add attributes and properties to element - forOwn(opts, function(prop, val){ - // only add if NOT in 'skip' array - if (skip.indexOf(prop) === -1){ - el[prop] = val; - } - }); - - // explicitly add element attributes - if (opts.attr){ - forOwn(opts.attr, function(name, val){ - el.setAttribute(name, val); - }); - } - - // explicitly add 'data-' attributes - if (opts.data){ - forOwn(opts.data, function(name, val){ - setElementData(el, name, val); - }); - } - - // execute element methods last... - // attach object or array of methods - // to 'fn' property - this can be an - // array in case you want to run the - // same method(s) more than once - if (opts.fn){ - [].concat(opts.fn).forEach(function(fn){ - forOwn(fn, function(f, args){ - el[f].apply(el, [].concat(args)); - }); - }); - } - - if (errors.length){ - if (console && console.log) console.log(errors) - } - - return el; - - }; - // aliases - spawn.lite = spawn.plus; - spawn.alt = spawn.plus; - - /** - * Spawn an HTML string using input parameters - * Simple but not super fast - * @param tag {String} tag name for HTML element - * @param [attrs] {Object|Array|String} element attributes - * @param [content] {String|Array} string or array of strings for HTML content - * @returns {String} HTML string - */ - spawn.html = function(tag, attrs, content){ - // the 'template' method can be useful - // for easily churning out plain old HTML - // no event handlers or other methods - - tag = tag || 'div'; - attrs = attrs || null; - content = content || []; - - var output = {}; - output.inner = ''; - output.attrs = ''; - - // use these as a shortcut to create <input> elements: - // spawn.html('input|text') - // - var inputTags = inputTypes.map(function(type){ - return 'input|' + type; - }); - - if (inputTags.indexOf(tag) > -1){ - tag = tag.split('|'); - output.attrs += (' type="' + tag[1] +'"'); - tag = tag[0]; - // maybe set 'content' as the value? - output.attrs += (' value="' + content + '"'); - // add content to [data-content] attribute? - //output.attrs += (' data-content="' + content + '"'); - } - - var isVoid = voidElements.indexOf(tag) > -1; - - if (inputTypes.indexOf(tag)) - - if (isVoid){ - output.open = '<' + tag; - output.close = '>'; - } - else { - output.open = '<' + tag; - output.inner = '>' + [].concat(content).join(' '); - output.close = '</' + tag + '>'; - } - - // process the attributes; - if (attrs){ - if (isPlainObject(attrs)){ - forOwn(attrs, function(attr, val){ - if (boolAttrs.indexOf(attr) > -1){ - if (attr){ - // boolean attributes don't need a value - output.attrs += (' ' + attr); - } - } - else { - output.attrs += (' ' + attr + '="' + val + '"'); - } - }); - } - else { - output.attrs += [''].concat(attrs).join(' '); - } - } - - return output.open + output.attrs + output.inner + output.close; - }; // convenience alias - spawn.fragment = function(el){ + spawn.fragment = spawn.lite.fragment = function(el){ var frag = doc.createDocumentFragment(); if (el){ frag.appendChild(el); diff --git a/src/main/webapp/scripts/utils.js b/src/main/webapp/scripts/utils.js index a7c09742693dd4ae232b34bdf0ccb3010c2d3bb0..2b7a89d9c8abb26d68ced3b7a9a6268578c9352a 100644 --- a/src/main/webapp/scripts/utils.js +++ b/src/main/webapp/scripts/utils.js @@ -350,27 +350,37 @@ function $$( el, id_prefix ){ // can't decide on a prefix for selection by id // use ONE of these: // id= | id: | @id= | @# | @= | @: | @ | #= | #: | #/ - var ALL_PREFIX = /^!\*/, // $$('!*div.foo') --> return all 'div.foo' elements as an array - RAW_ID = /^!#/, // $$('!#foo') --> return (one) element with id 'foo' - RAW_PREFIX = /^!/, // $$('!div.foo') --> return FIRST 'div.foo' element - ID_PREFIX = /^(id=|id:|@id=|@#|@=|@:|@|#=|#:|#\/)/; + var ALL_PREFIX = /^!\*/, // $$('!*div.foo') --> return raw 'div.foo' elements as an array + RAW_ID = /^!#/, // $$('!#foo') --> return (one) raw element with id 'foo' + NAME_RAW = /^!\?/, // $$('!?foo') --> return raw elements with [name="foo"] + NAME_$ = /^\?/, // $$('?foo') --> return wrapped elements with [name="foo"] + RAW_PREFIX = /^!/, // $$('!div.foo') --> return FIRST raw 'div.foo' element + ID_PREFIX = /^(id=|id:|@id=|@#|@=|@:|@|#=|#:|#\/)/; if (!el || el.jquery){ return el; } - if (el.search(ALL_PREFIX) === 0){ - return document.querySelectorAll(el.replace(ALL_PREFIX, '')); - } - // pass empty string or null as the second argument - // to get the bare element by id (no jQuery) - if (id_prefix === '' || id_prefix === null || el.search(RAW_ID) === 0){ - return document.getElementById(el.replace(RAW_ID,'')); - } - if (el.search(RAW_PREFIX) === 0){ - return document.querySelector(el.replace(RAW_PREFIX,'')); - } - id_prefix = id_prefix || ID_PREFIX; - if (el.search(id_prefix) === 0){ - return $(document.getElementById(el.replace(id_prefix,''))); + if (typeof el == 'string'){ + if (el.search(ALL_PREFIX) === 0){ + return document.querySelectorAll(el.replace(ALL_PREFIX, '')); + } + // pass empty string or null as the second argument + // to get the bare element by id (no jQuery) + if (id_prefix === '' || id_prefix === null || el.search(RAW_ID) === 0){ + return document.getElementById(el.replace(RAW_ID,'')); + } + if (el.search(NAME_RAW) === 0){ + return document.getElementsByName(el.replace(NAME_RAW, '')); + } + if (el.search(NAME_$) === 0){ + return $(document.getElementsByName(el.replace(NAME_$, ''))); + } + if (el.search(RAW_PREFIX) === 0){ + return document.querySelector(el.replace(RAW_PREFIX,'')); + } + id_prefix = id_prefix || ID_PREFIX; + if (el.search(id_prefix) === 0){ + return $(document.getElementById(el.replace(id_prefix,''))); + } } return $(el); } diff --git a/src/main/webapp/scripts/xnat/app/scriptEditor.js b/src/main/webapp/scripts/xnat/app/scriptEditor.js index e18c92d839f921f4e5a903295a9f9d9baac5d4a7..2de23f98855b32e5227aae370dee3190e3f2f5ea 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/scripts/xnat/element.js b/src/main/webapp/scripts/xnat/element.js index 2b777b2d2d07a69a8fecb19a71bc0f00f6420233..797fe83ed0b0d8e9fa9e8b5ab91ac3d32b54ea87 100644 --- a/src/main/webapp/scripts/xnat/element.js +++ b/src/main/webapp/scripts/xnat/element.js @@ -54,18 +54,40 @@ var XNAT = getObject(XNAT||{}); return this; }; + var $methods = [ + 'attr', 'prepend', 'append', + 'before', 'after', + 'addClass', 'removeClass' + ]; + + // hijack some methods from jQuery + $methods.forEach(function(method){ + Element.p[method] = function(){ + var $el = this.getLast$(); + $el[method].apply($el, arguments); + return this; + } + }); + + // uses jquery.dataAttr.js + Element.p.data = function(name, value){ + this.lastElement = this.lastElement || this.parent || this.rootElement; + $(this.lastElement).dataAttr(name, value); + return this; + }; + // return root element and all children - Element.p.get = function(){ + Element.p.get = function(i){ if (this.isFragment){ if (this.rootElement.childNodes.length){ - return this.rootElement.childNodes; + return this.rootElement.childNodes[i||0]; } } return this.rootElement; }; - Element.p.get$ = function(){ - return $(this.get()) + Element.p.get$ = function(i){ + return $(this.get(i)) }; // return last element in the chain @@ -77,7 +99,24 @@ var XNAT = getObject(XNAT||{}); }; Element.p.getLast$ = function(){ - return $(this.getLast()) + this.$last = this.last$ = $(this.getLast()); + return this.$last; + }; + + Element.p.$ = function($opts){ + var $last = this.getLast$(); + if (arguments.length === 2){ + $last[arguments[0]].apply($last, [].concat(arguments[1])); + } + else if (Array.isArray($opts)){ + $last[$opts[0]].apply($last, [].concat($opts[1])); + } + else { + forOwn($opts, function($opt, args){ + $last[$opt].apply($last, [].concat(args)); + }); + } + return this; }; Element.p.upTo = Element.p.up = function(tag){ @@ -105,12 +144,61 @@ var XNAT = getObject(XNAT||{}); return this; }; + ////////////////////////////////////////////////////////////////////// // chainable spawner // XNAT.element('div').p()._b('Bold text. ')._i('Italic text.'); // -> <div><p><b>Bold text. </b><i>Italic text.</i></p></div> XNAT.element = XNAT.el = element = function(tag, opts, content){ return new Element(tag, opts, content); }; + ////////////////////////////////////////////////////////////////////// + + // copy value from 'target' to 'source' + element.copyValue = function(target, source){ + var sourceValue = $$(source).val(); + $$(target).val(sourceValue).dataAttr('value', sourceValue); + }; + + // copy value from 'source' to last element in the chain + Element.p.copyValue = function(source){ + element.copyValue(this.getLast$(), source); + return this; + }; + + // set 'name' property with 'value' on 'elements' + element.setProp = function(elements, name, value){ + [].concat(elements).forEach(function(element){ + $$(element).prop(name, value); + }); + }; + + // set properties and classes to 'disable' an element + element.setDisabled = function(elements, disabled){ + disabled = firstDefined(disabled, true); + [].concat(elements).forEach(function(element){ + var _disabled = !!disabled; + var modifyClass = _disabled ? 'addClass' : 'removeClass'; + $$(element).prop('disabled', _disabled)[modifyClass]('disabled'); + }); + }; + + // 'disable' last element in the chain + Element.p.disabled = Element.p.setDisabled = function(disabled){ + element.setDisabled(this.getLast$(), disabled); + return this; + }; + + element.checked = function(elements, checked){ + [].concat(elements).forEach(function(element){ + var _checked = !!checked; + var modifyClass = _checked ? 'addClass' : 'removeClass'; + $$(element).prop('checked', _checked)[modifyClass]('checked'); + }); + }; + + Element.p.checked = function(checked){ + + }; // space-separated list of elements // for auto-generated functions @@ -126,7 +214,7 @@ var XNAT = getObject(XNAT||{}); 'ul ol li dl dt dd hr br iframe ' + 's small sub sup u b i em strong pre ' + 'form fieldset button input textarea ' + - 'select option optgroup ' + + 'label select option optgroup ' + 'img map area embed object script' + '').split(/\s+/); @@ -146,15 +234,11 @@ var XNAT = getObject(XNAT||{}); return this; }; - //// add siblings - //Element.p['_'+tag] = function(opts, content){ - // var el = setupElement(tag, opts, content); - // this.lastParent = this.lastElement.parentNode; - // this.lastParent.appendChild(el); - // this.parent = el; - // this.lastElement = el; - // return this; - //}; + Element.p['_'+tag+'$'] = function(opts, content){ + this.last$ = this.$last = + this['_'+tag]().$(opts).get$().append(content); + return this; + }; // add generators to prototype for chaining Element.p[tag] = function(opts, content){ @@ -178,8 +262,4 @@ var XNAT = getObject(XNAT||{}); }); - // TODO: make element methods chainable: - // XNAT.element.div({id:'foo'}).p({className:'bar'}, 'Foo Bar'); - // --> <div id="foo"><p class="bar">Foo Bar</p></div> - })(XNAT); diff --git a/src/main/webapp/scripts/xnat/ui/panel.js b/src/main/webapp/scripts/xnat/ui/panel.js new file mode 100644 index 0000000000000000000000000000000000000000..d8d66b1dde0638357b3d9f533c2f0dc62b2871f6 --- /dev/null +++ b/src/main/webapp/scripts/xnat/ui/panel.js @@ -0,0 +1,634 @@ +/*! + * Functions for creating XNAT tab UI elements + */ + +var XNAT = getObject(XNAT||{}); + +(function(XNAT, $, window, undefined){ + + var spawn = window.spawn, + element = XNAT.element; + + + /** + * Initialize panel and optionally render it. + * If 'opts' argument is passed, 'setup' method will run. + * If 'container' argument is passed, the 'render' method will run. + * So if both args are passed, 'setup' and 'render' do NOT need to be called. + * @param [opts] {Object} Config object + * @param [container] {Element} Container for panel + * @returns {{}} + */ + function panel(opts, container){ + + // `this` object + var __ = {}; + + /** + * Standard panel widget + */ + function newPanel(){ + + var sections = [ + ['div.panel-heading', [ + ['h3.panel-title', __.opts.title] + ]], + ['div.panel-body', __.opts.body] + ]; + + if (__.opts.footer){ + sections.push(['div.panel-footer', __.opts.footer]) + } + + return spawn((__.opts.tag) + '.panel.panel-default', __.opts.attr, sections); + //return $(spawn('div.panel.panel-default')).append(content); + } + + /** + * Sets up elements before rendering to the page + * @param _opts Config object + * @returns {{}} + */ + __.setup = function(_opts){ + + __.opts = extend(true, {}, _opts); + + __.opts.tag = __.opts.tag || 'div'; + __.opts.title = __.opts.title || __.opts.header || ''; + __.opts.body = __.opts.body || __.opts.content || ''; + __.opts.footer = __.opts.footer || ''; + + __.opts.attr = __.opts.attr || {}; + + if (__.opts.id){ + __.opts.attr.id = __.opts.id + } + + if (__.opts.name){ + __.opts.attr.data = getObject(__.opts.attr.data); + __.opts.attr.data.name = __.opts.name; + } + + __.panel = __.element = newPanel(); + + return __; + }; + + // if 'opts' arg is passed to .panel(), call .setup() + if (opts){ + __.setup(opts); + } + + // render the panel and append to 'container' + __.render = function(container){ + $$(container).append(__.panel); + return __; + }; + + // render immediately if 'container' is specified + if (container){ + __.render(container); + } + + __.get = function(){ + return __.element; + }; + + return __; + + } + + + + /** + * Panel widget with default 'Submit' and 'Revert' buttons + * @param opts + * @param container + * @returns {*} + */ + panel.form = function(opts, container){ + + var __ = {}, + _panel, $panel, + saveBtn, revertBtn, + $saveBtn, $revertBtn; + + opts.tag = 'form.xnat-form-panel'; + + opts.title = opts.label; + + opts.body = []; + + if (opts.description){ + opts.body.push(element.p(opts.description||'')) + } + + if (opts.elements){ + opts.body = opts.body.concat(setupElements(opts.elements)) + } + + saveBtn = footerButton('Save', 'submit', true, 'save pull-right'); + revertBtn = footerButton('Discard Changes', 'button', true, 'revert pull-right'); + + opts.footer = [ + saveBtn, + spawn('span.pull-right', ' '), + revertBtn, + footerButton('Default Settings', 'link', false, 'defaults pull-left'), + spawn('div.clear') + ]; + + // CREATE THE PANEL + _panel = panel(opts); + + $panel = $(_panel.element); + $saveBtn = $(saveBtn); + $revertBtn = $(revertBtn); + + // what's the submission method? + var method = opts.method || 'GET'; + + method = method.toUpperCase(); + + var url = '#'; + + if (method === 'GET'){ + url = XNAT.url.restUrl(opts.url) + } + else if (/PUT|POST|DELETE/.test(method)) { + // add CSRF token for PUT, POST, or DELETE + url = XNAT.url.csrfUrl(opts.url) + } + else { + // any other 'method' value is ignored + } + + // set panel as 'dirty' when stuff changes + $panel.on('change', ':input', function(){ + $(this).addClass('dirty'); + setDisabled($panel.find('.panel-footer button'), false); + }); + + $panel.on('submit', function(e){ + e.preventDefault(); + // submit the data and disable 'Save' and 'Revert' buttons + if (opts.url) { + try { + XNAT.xhr.request({ + method: method, + url: url, + data: $panel, + success: function(){ + console.log('success!'); + setDisabled([$saveBtn, $revertBtn], true); + }, + error: function(){ + console.log('error.') + } + }) + } + catch(e) { + setDisabled([$saveBtn, $revertBtn], true); + console.log(e) + } + } + }); + + _panel.revert = function(){ + discardChanges($panel); + setDisabled([$saveBtn, $revertBtn], true); + }; + + $revertBtn.on('click', _panel.revert); + + _panel.panel = _panel.element; + + return _panel + + }; + + function footerButton(text, type, disabled, classes){ + var button = { + type: type||'button', + html: text||'Submit' + }; + button.classes = [classes||'', 'btn btn-sm']; + if (type === 'link'){ + button.classes.push('btn-link') + } + else if (/submit|primary/.test(type)){ + button.classes.push('btn-primary') + } + else { + button.classes.push('btn-default') + } + if (disabled) { + button.classes.push('disabled'); + button.disabled = 'disabled' + } + return spawn('button', button); + } + + // return a single 'toggle' element + function radioToggle(item){ + + var element = extend(true, {}, item), + radios; + + // don't add these properties to the wrapper + element.name = null; + element.label = null; + + //fieldset.tag = 'div.radio-toggle'; + //element.classes = 'show-selected'; + + item.name = item.name || randomID(); + + radios = item.options.map(function(radio){ + + var label = {}, + button = spawn('input', { + type: 'radio', + name: item.name, + value: radio.value + }), + description = spawn('div.description', { + data: { 'for': item.value }, + title: item.value, + html: radio.description + }); + + if (button.value === item.value){ + button.checked = true; + } + + if (!button.checked){ + $(description).addClass('hidden'); + //button.disabled = true; + //label.classes = 'hidden'; + } + + $(button).on('click', function(){ + $(this).closest('.radio-toggle').find('.description').addClass('hidden'); + $(description).removeClass('hidden'); + alert('foo'); + }); + //button.onchange = ; + + label.append = description; + + return ['label.radio-item', label, [button, ' '+radio.label]]; + + }); + + return spawn('div', element, radios); + + } + + // return a single panel element + function panelElement(item){ + + var elements = [], + element, tag, + children = '', + before = [], + after = [], + kind = item.kind || '', + obj = {}; + + // input (or other) element + tag = item.tag||item.kind||'div'; + + if (kind === 'element-group' && item.elements.length){ + element = groupElements(item.elements); + //element = spawn(tag, [radioToggle(item)]); + } + else { + if (item.name){ + obj.name = item.name + } + + if (item.type){ + obj.type = item.type; + } + } + + if (item.id){ + obj.id = item.id; + } + + if (tag === 'input' && !item.type){ + obj.type = 'text'; + } + + // 'checkbox' kind + if (kind === 'checkbox'){ + tag = 'input'; + obj.type = 'checkbox'; + } + + // set a default 'size' value for text inputs + if (tag === 'input' && /text|email|url/.test(item.type||obj.type||'')){ + obj.size = '25'; + } + + if (item.label){ + obj.title = item.label; + } + + obj.data = item.data ? extend(true, {}, item.data) : {}; + + if (item.value){ + obj.value = item.value; + obj.data.value = item.value; + } + + if (item.checked){ + obj.checked = true; + obj.data.state = 'checked'; + } + + if (item.info){ + obj.data.info = item.info; + } + + if (item.attr || item.attributes){ + obj.attr = item.attr || item.attributes || {}; + } + + if (/form-table|inputTable|input-table/i.test(kind)){ + element = XNAT.ui.inputTable(item.tableData).get(); + } + else { + obj.innerHTML = [].concat(item.innerHTML || item.html || []).join('\n'); + } + + if (item.before){ + console.log('before'); + before = item.before; + //elements.push(spawn('span.before', item.before)) + } + + if (item.after){ + console.log('after'); + after = item.after; + //elements.push(spawn('span.after', item.after)) + } + + if (kind !== 'hidden'){ + // enable the 'Save' and 'Discard Changes' buttons on change + obj.onchange = function(){ + var $panel = $(this).closest('.panel'); + setDisabled([$panel.find('.save'), $panel.find('.revert')], false); + }; + } + + if (kind === 'select' && item.options){ + children = item.options.map(function(option){ + var obj = {}; + obj.value = option.value; + obj.html = option.label; + if (isDefined(item.value)){ + if (item.value === obj.value){ + obj.selected = true; + } + } + return ['option', obj] + }); + } + + element = element || spawn(tag, obj, children); + + if (!elements.length){ + elements = [].concat(before, element, after); + } + // add a description if present + if (item.description){ + elements.push(spawn('div.description', item.description)) + } + + // element setup + return elements; + + } + + function elementLabel(label, id){ + var obj = { + innerHTML: label||'' + }; + if (id){ + obj.attr = { + 'for': id + } + } + return spawn('label.element-label', obj); + } + + // create elements that are part of an 'element-group' + // returns Spawn arguments array + function groupElements(items){ + return items.map(function(item){ + var label = ''; + var tag = item.kind === 'hidden' ? 'div.hidden' : 'div.group-item'; + if (item.label){ + label = elementLabel(item.label, item.id) + } + tag += '|data-name=' + item.name; + return [tag, [].concat(label, panelElement(item))]; + }); + } + + // create elements from the 'elements' array + // returns Spawn arguments array + function setupElements(items){ + return items.map(function(item){ + var label = ''; + var tag = 'div.panel-element'; + switch (item.kind) { + case 'hidden': + tag = 'div.hidden'; + break; + case 'element-group': + tag += '.element-group'; + break; + } + if (item.label){ + label = elementLabel(item.label, item.id) + } + tag += '|data-name=' + item.name; + return [tag, [label, ['div.element-wrapper', panelElement(item)]]]; + }); + } + + function discardChanges(form){ + + var $form = $$(form); + + // reset all checkboxes and radio buttons + $form.find(':checkbox, :radio').each(function(){ + var $this = $(this); + if ($this.hasClass('dirty')){ + if ($this.data('state') !== 'checked'){ + if ($this.is(':checked')){ + $this.trigger('click') + } + else { + // nevermind. + } + } + $this.removeClass('dirty'); + } + }); + + // reset text inputs based on [data-value] attribute + $form.find(':input').not(':checkbox, :radio').val(function(){ + return $(this).data('value'); + }).trigger('change').removeClass('dirty'); + + // simulate click on items that were initially checked + //$form.find(':input[data-state="checked"]').trigger('click'); + + } + + function setValue(target, source){ + $$(target).val($$(source).val()); + } + + function toggleValue(target, source, modifier){ + + var $source = $$(source); + var $target = $$(target); + + var sourceVal = $source.val(); + var targetVal = $target.val(); + var dataVal = $target.data('value'); + + $target[0].value = (targetVal === sourceVal) ? dataVal : sourceVal; + + // avoid infinite loop of change triggers + if ($target[0] !== $$(modifier)[0]){ + $target.trigger('change.modify'); + } + + } + + + function setDisabled(elements, disabled){ + [].concat(elements).forEach(function(element){ + var _disabled = !!disabled; + var modifyClass = _disabled ? 'addClass' : 'removeClass'; + $$(element).prop('disabled', _disabled)[modifyClass]('disabled'); + }); + } + + + function setHidden(elements, hidden){ + [].concat(elements).forEach(function(element){ + var showOrHide, modifyClass; + if (!!hidden){ + showOrHide = 'hide'; + modifyClass = 'addClass'; + } + else { + showOrHide = 'show'; + modifyClass = 'removeClass'; + } + $$(element)[showOrHide]()[modifyClass]('hidden'); + }); + } + + + XNAT.ui = getObject(XNAT.ui || {}); + XNAT.ui.panel = panel.form; // temporarily use the 'form' panel kind + XNAT.ui.panelForm = XNAT.ui.formPanel = panel.form; + + + $(function(){ + + var $body = $('body'); + + // bind the XNAT.event.toggle() function to elements with 'data-' attributes + $body.on('change.modify', '[data-modify]', function(){ + + var $this = $(this); + var checked = $this.is(':checked'); + + // allow multiple states to be passed - separated by semicolons + var states = $this.data('modify').split(';'); + + states.forEach(function(set){ + + var parts = set.split(':'); + + var state = parts[0].trim(); + + // allow multiple arguments as a comma-separated list + var args = parts[1].split(','); + + var _target = args[0].trim(); + var _source = (args[1]||'').trim() || _target; + + if (args[1]){ + _source = args[1].trim(); + } + + switch (state) { + + case 'toggle.disabled' || 'toggle.disable': + setDisabled(_target, checked); + break; + + case 'toggle.enabled' || 'toggle.enable': + setDisabled(_target, !checked); + break; + + case 'disable' || 'disabled': + setDisabled(_target, true); + break; + + case 'enable' || 'enabled': + setDisabled(_target, false); + break; + + case 'toggle.hidden' || 'toggle.hide': + setHidden(_target, checked); + break; + + case 'toggle.visible' || 'toggle.show': + setHidden(_target, !checked); + break; + + case 'hide': + setHidden(_target, true); + break; + + case 'show': + setHidden(_target, false); + break; + + case 'toggle.value': + toggleValue(_target, _source, $this[0]); + break; + + case 'apply.value' || 'set.value': + setValue(_target, $this); + break; + + case 'get.value': + setValue($this, _target); + break; + + } + + }) + + }); + + // trigger a change on load? + //$('[data-modify]').trigger('change.modify'); + + }); + + + +})(XNAT, jQuery, window); \ No newline at end of file diff --git a/src/main/webapp/scripts/xnat/ui/table.js b/src/main/webapp/scripts/xnat/ui/table.js new file mode 100644 index 0000000000000000000000000000000000000000..b693362d6da1c8a39f9d6eedcc80df522f6dc829 --- /dev/null +++ b/src/main/webapp/scripts/xnat/ui/table.js @@ -0,0 +1,321 @@ +/*! + * Methods for creating XNAT-specific <table> elements + */ + +var XNAT = getObject(XNAT||{}); + +(function(XNAT, $){ + + var table, + element = spawn.element, + undefined; + + /** + * Constructor function for XNAT.table() + * @param [opts] {Object} < table > Element attributes + * @param [config] {Object} other config options + * @constructor + */ + function Table(opts, config){ + + this.opts = opts||{}; + this.config = config||null; + + this.table = element('table', this.opts); + this.table$ = $(this.table); + + this.last = {}; + + // 'parent' gets reset on return of chained methods + this.last.parent = this.table; + + // get 'last' item wrapped in jQuery + this.last$ = function(el){ + return $(this.last[el||'parent']); + }; + + this.setLast = function(el){ + this.last.parent = + this.last[el.tagName.toLowerCase()] = + el; + }; + + this._rows = []; + this.cols = this.columns = []; + + // try to init? + if (config && config.data){ + this.init(config.data); + } + } + + + // alias prototype for less typing + Table.p = Table.prototype; + + + // jQuery methods we'd like to use: + var $methods = [ + 'append', + 'prepend', + 'addClass', + 'find' + ]; + + $methods.forEach(function(method){ + Table.p[method] = function(){ + var $el = this.last$(); + $el[method].apply($el, arguments); + return this; + } + }); + + // create a single <td> element + Table.p.td = function(content, opts){ + var td = element('td', opts||content, content); + this.last.tr.appendChild(td); + //this.setLast(td); + return this; + }; + + Table.p.th = function(content, opts){ + var th = element('th', opts||content, content); + this.last.tr.appendChild(th); + //this.setLast(th); + return this; + }; + + Table.p.tr = function(data, opts){ + var tr = element('tr', opts); + //data = data || this.data || null; + if (data){ + [].concat(data).forEach(function(item){ + tr.appendChild(element('td', item)) + }); + } + // only add <tr> elements to <table>, <thead>, <tbody>, and <tfoot> + if (/(table|thead|tbody|tfoot)/.test(this.last.parent.tagName.toLowerCase())){ + this.last.parent.appendChild(tr); + } + this.last.tr = tr; + //this.setLast(tr); + return this; + }; + + // create a row with <tr> and <td> elements + // in the <tbody> + Table.p.row = function(data, opts){ + var tr = element('tr', opts); + data = data || []; + [].concat(data).forEach(function(item){ + tr.appendChild(element('td', item)); + }); + (this.last.tbody||this.table).appendChild(tr); + return this; + }; + + // create *multiple* <td> elements + Table.p.tds = function(items, opts){ + var last_tr = this.last.tr; + [].concat(items).forEach(function(item){ + var td; + if (isPlainObject(item)){ + td = element('td', '', extend(true, item, opts)); + } + else { + td = element('td', item, opts); + } + last_tr.appendChild(td); + }); + // don't reset 'last' so we + // keep using the parent <tr> + return this; + }; + + Table.p.rows = function(data, opts){ + var _this = this, + rows = []; + data = data||[]; + data.forEach(function(row){ + rows.push(_this.tr(opts, row)) + }); + this._rows = rows; + this.append(this._rows); + return this; + }; + + Table.p.thead = function(opts, data){ + var head = element('thead', opts); + this.table.appendChild(head); + this.setLast(head); + return this; + }; + + Table.p.tbody = function(opts, data){ + var body = element('tbody', opts); + this.table.appendChild(body); + this.setLast(body); + return this; + }; + + // reset last.parent to <tbody> + Table.p.toBody = Table.p.closestBody = function(){ + this.setLast(this.last.tbody||this.table); + return this; + }; + + // reset last.parent to <thead> + Table.p.toHead = Table.p.closestBody = function(){ + this.setLast(this.last.thead||this.table); + return this; + }; + + Table.p.bodyRow = function(){ + this.toBody(); + this.tr(); + return this; + }; + + // add a SINGLE row of data + Table.p.addRow = function(data){ + var _this = this; + this.tr(); + [].concat(data).forEach(function(item){ + // could be an array of arrays + _this.td(item); + }); + return this; + }; + + // add multiple rows of data? + Table.p.appendBody = function(data){ + var _this = this; + [].concat(data).forEach(function(row){ + _this.toBody(); + _this.addRow(row); + }); + return this; + }; + + Table.p.get = function(){ + return this.table; + }; + + Table.p.get$ = function(){ + return $(this.table); + }; + + /** + * Populate table with data + * @param data {Array} array of row arrays + * @returns {Table.p} Table.prototype + */ + Table.p.init = function(data){ + + var _this = this, + obj = {}, + header, + cols = 0; + + // don't init twice + if (this.inited) { return this } + + data = data || []; + + if (Array.isArray(data)){ + obj.data = data; + } + else { + obj = data||{}; + } + if (obj.header){ + // if there's a 'header' property + // set to true, pick the header from + // the first row of data + if (obj.header === true){ + header = obj.data.shift(); + } + // otherwise it's set explicitly + // as an array in the 'header' property + // and that sets the number of columns + else { + header = obj.header; + } + } + + // set the number of columns based on + // the header or first row of data + cols = (header) ? header.length : (obj.data[0]||[]).length; + + // add the header + if (header){ + this.thead(); + this.tr(); + [].concat(header).forEach(function(item){ + _this.th(item); + }); + } + + // always add <tbody> element on .init() + this.tbody(); + + [].concat(obj.data||[]).forEach(function(col){ + var i = -1; + // make a row! + _this.tr(); + // don't exceed column width of header or first column + while (++i < cols){ + _this.td(col[i]); + } + }); + + this.inited = true; + + return this; + + }; + + table = function(data, opts){ + if (!opts){ + opts = data; + data = []; + } + return new Table(data, opts); + }; + + // helper for future XNAT DataTable widget + table.dataTable = function(data, opts){ + + }; + + // table with <input> elements in the cells + table.inputTable = function(data, opts){ + data = data.map(function(row){ + return row.map(function(cell){ + if (/string|number/.test(typeof cell)){ + return cell+'' + } + if (Array.isArray(cell)){ + return element('input', extend(true, {}, cell[2], { + name: cell[0], + value: cell[1], + data: { value: cell[1] } + })); + } + cell = extend(true, cell, { + data: { value: cell.value } + }); + return element('input', cell); + }); + }); + opts = getObject(opts); + opts.className = 'input-table'; + var table = new Table(opts); + return table.init(data); + }; + + XNAT.ui = getObject(XNAT.ui); + XNAT.ui.table = XNAT.table = table; + XNAT.ui.inputTable = XNAT.inputTable = table.inputTable; + +})(XNAT, jQuery); diff --git a/src/main/webapp/scripts/xnat/ui/tabs.js b/src/main/webapp/scripts/xnat/ui/tabs.js new file mode 100644 index 0000000000000000000000000000000000000000..1cef6d4fb219644ff3a3762b84a3fa84327fab3b --- /dev/null +++ b/src/main/webapp/scripts/xnat/ui/tabs.js @@ -0,0 +1,333 @@ +/*! + * Functions for creating XNAT tab UI elements + */ + +var XNAT = getObject(XNAT||{}); + +(function(XNAT, $, window, undefined){ + + var ui, tabs, page, + element = XNAT.element; + + var $body = $(document.body); + + XNAT.ui = ui = getObject(XNAT.ui || {}); + XNAT.ui.tabs = XNAT.tabs = tabs = getObject(XNAT.ui.tabs || {}); + XNAT.page = page = getObject(XNAT.page || {}); + + + /** + * Initialize the tabs + * @param [tabsArray] {Array} array of tab config objects + * @param [container] {Element} parent element for tabs + * @returns {{}} + */ + function init(tabsArray, container){ + + // a place to store things locally + var __ = {}; + + // keep tabs on ALL tabs + __.tabs = {}; + + // keep tabs on _each_ tab + __.tab = {}; + + + // __.tab['tabName'].name = 'name'; + // __.tab['tabName'].label = 'Label'; + // __.tab['tabName'].id = 'id'; // is this redundant? + // __.tab['tabName'].flipper = spawn('li.tab'); + // __.tab['tabName'].content = spawn('div.tab-pane-content', content); + // __.tab['tabName'].pane = spawn('div.tab-pane', [['div.pad', __.tab['tabName'].content]]); + + + function activateTab(name){ + + var tab = __.tab[name]; + + // first deactivate ALL tabs and panes + + __.tabs.$panes + .find('.tab-pane') + .hide() + .removeClass('active'); + + __.tabs.$flippers + .find('.tab') + .removeClass('active'); + + // then activate THIS tab and pane + + $(tab.flipper.wrapper) + .addClass('active'); + + $(tab.pane.wrapper) + .show() + .addClass('active'); + + __.tabs.activeTab = name; + + } + + + function refreshData(form, url){ + + } + + + function paneFooter(){ + + var footer = spawn('footer.footer', [ + ['button', { + type: 'button', + html: 'Save All', + classes: 'save-all btn btn-primary pull-right' + }] + ]); + + return footer; + + } + + + // spawn sample elements for a pane + function sampleContent(name, content){ + var pane = __.tab[name].pane.content; + var list = []; + [].concat(content).forEach(function(item){ + list.push(spawn('li', { + innerHTML: item.label + ': ', + id: item.id, + data: { + name: item.name, + kind: item.kind + }, + append: element.p(item.description||'') + })) + }); + pane.appendChild(spawn('ul', list)); + } + + + // add content to pane + function paneContents(contents){ + return [].concat(contents).map(function(item){ + // "kind" property defines which + // kind of UI widget to render + // default is "panel" + var widget = item.kind||'panel'; + return ui[widget](item).element; + }); + } + + + // set up a single pane + function setupPane(name){ + var tab = __.tab[name]; + tab.pane = {}; + tab.pane.content = spawn('div.tab-pane-content'); + tab.pane.wrapper = spawn('div.tab-pane', { + id: tab.id + '-content' + }, [ + ['div.pad', [tab.pane.content]] + ]); + + // add pane header + // or don't... + //tab.pane.content.appendChild(element.h3(tab.label)); + + // add contents + if (tab.contents){ + $(tab.pane.content).append(paneContents(tab.contents)); + } + + //sampleContent(tab.name, tab.contents||[]); + + // add styles so footer is pinned at the bottom + // move these to CSS eventually + tab.pane.wrapper.style.position = 'relative'; + tab.pane.wrapper.style.paddingBottom = '60px'; + + // append footer last + tab.pane.wrapper.append(paneFooter()); + + return tab.pane; + } + + + // create <ul> elements for tab groups + function setupGroups(list){ + + var flippers = __.tabs.flippers.container; + + __.tabs.groups = {}; + + list.forEach(function(item){ + + var name = item.name; + var label = item.label; + var id = toDashed(name) + '-tabs'; + + var container = spawn('ul.nav.tab-group', { + id: id, + html: '<li class="label">' + label + '</li>' + }); + + flippers.appendChild(container); + + __.tabs.groups[name] = { + name: name, + label: label, + id: id, + container: container + }; + + }); + } + + + // set up a single flipper + function setupFlipper(name){ + var tab = __.tab[name]; + tab.flipper = {}; + tab.flipper.wrapper = spawn('li.tab'); + tab.flipper.label = + tab.flipper.a = + spawn('a', { + innerHTML: tab.label, + title: tab.label, + href: '#' + tab.id, + onclick: function(){ + activateTab(name); + } + }); + if (tab.isDefault && !__.tabs.activeTab){ + activateTab(name); + } + tab.flipper.wrapper.appendChild(tab.flipper.label); + return tab.flipper; + } + + + /** + * Process JSON and setup flippers and panes to render + * @param config {Array} array of tabs + */ + function setupTabs(config){ + + var frag = spawn.fragment(); + var panes = $$('!div.xnat-tab-content'); // get existing element + //var panes = spawn('div.xnat-tab-content.xnat-tab-panes'); + //var panes = element.div({className:'xnat-tab-content xnat-tab-panes'}); + var flippers = $$('!div.xnat-nav-tabs'); // get existing element + //var flippers = spawn('ul.nav.xnat-nav-tabs'); + //var flippers = element.ul({className:'nav xnat-nav-tabs'}); + + // expose to outer scope + __.tabs.frag = frag; + __.tabs.panes = { + container: panes + }; + __.tabs.flippers = { + container: flippers + }; + __.tabs.config = config; + + __.tabs.$panes = $(panes); + __.tabs.$flippers = $(flippers); + + function setLayout(side){ + // only 'left' or 'right' + if (!/left|right/.test(side)) return; + var other = side === 'left' ? 'right' : 'left'; + __.tabs.$flippers.addClass('side pull-'+side); + __.tabs.$panes.addClass('side pull-'+other); + } + + [].concat(config).forEach(function(item){ + + if (item.kind !== 'tab') { + if (item.kind === 'meta'){ + if (item.groups){ + // setup tab groups + setupGroups(item.groups); + } + if (item.layout){ + setLayout(item.layout); + } + } + return; + } + + var _tab = extend(true, {}, item); + var _name = item.name || randomID('tab-', false); + + _tab.name = _name; + _tab.label = item.label || titleCase(_name); + _tab.id = item.id || toDashed(_name); + + // save the first tab to activate it if there's no 'active' tab + if (!__.tabs.firstTab){ + __.tabs.firstTab = _name; + } + + // add THIS tab to the collection + __.tab[_name] = _tab; + + _tab.pane = setupPane(_name); + _tab.flipper = setupFlipper(_name); + + __.tabs.panes.container.appendChild(_tab.pane.wrapper); + __.tabs.groups[item.group].container.appendChild(_tab.flipper.wrapper); + + }); + + // set the first tab to active if no 'default' tab is set + if (!__.tabs.activeTab){ + activateTab(__.tabs.firstTab) + } + + frag.appendChild(flippers); + frag.appendChild(panes); + + return __; + + } + // expose globally + __.setup = setupTabs; + + // run setup on init() if 'tabsArray' is present + if (tabsArray && tabsArray.length){ + setupTabs(tabsArray); + } + + __.render = function(container){ + $$(container).append(__.tabs.frag); + // clone values + //$('[value^="@|"]').each(function(){ + // var selector = $(this).val().split('@|')[1]; + // var value = $$(selector).val(); + // $(this).val(value).dataAttr('value',value); + // $(this).change(); + //}); + return __; + }; + + // render immediately if 'container' is specified + if (container){ + __.render(container); + } + + // object to cache tab elements and data for quicker access + XNAT.page.tabs = __.tab; + + return __; + + } + + tabs.init = init; + +})(XNAT, jQuery, window); + + diff --git a/src/main/webapp/xnat-templates/screens/EditScript.vm b/src/main/webapp/xnat-templates/screens/EditScript.vm index 161e0523fa467611a843bbc16bd4fa017dda865c..57b4fc97034099a1c9f1788d694477cff2963ccc 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 db72d0dca2d0781ca9cfdf3afb2ec1b2a4278398..9186484e7d5456ffceb3aea20159dbd264e6fdfe 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;"> diff --git a/src/main/webapp/xnat-templates/screens/xnat_imageSessionData/xnat_imageSessionData_details.vm b/src/main/webapp/xnat-templates/screens/xnat_imageSessionData/xnat_imageSessionData_details.vm index ad1545f512fbda89b19b64a3e6f36aedb464f90c..fdbdbfaf5c193cd56ec200c3af124ae8a8bd2776 100644 --- a/src/main/webapp/xnat-templates/screens/xnat_imageSessionData/xnat_imageSessionData_details.vm +++ b/src/main/webapp/xnat-templates/screens/xnat_imageSessionData/xnat_imageSessionData_details.vm @@ -98,4 +98,19 @@ </tr> </table> #end +<button type="button" id="launchScript">Launch Script</button> +<script type="text/javascript"> + + scriptCallback={ + success:function(){alert("Script launched.")}, + failure:function(){alert("Script failed to launch.")}, + cache:false, // Turn off caching for IE + scope:this + } + + jQuery('#launchScript').click(function(event){ + var params="sessionId=" + "${om.getId()}" + "&mountIn=" + "/data/input" + "&mountOut=" + "/data/output" + "&imageId=" + "kelseym/pydicom"; + YAHOO.util.Connect.asyncRequest('POST',serverRoot +"/xapi/containers/launch/script/${om.getId()}?XNAT_CSRF=" + csrfToken,scriptCallback,params,this); + }); +</script> <!--END xnat_imageSessionData_details.vm --> \ No newline at end of file