Commits

John Paulett  committed 7fd51db

Create a new subject in XNAT from the applet.

  • Participants
  • Parent commits 90f38a9

Comments (0)

Files changed (10)

     <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
-      <version>3.8.1</version>
+      <version>4.8.1</version>
       <scope>test</scope>
     </dependency>
     <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-all</artifactId>
+      <version>1.8.5</version>
+      <scope>test</scope>
+	</dependency>
+    <dependency>
       <groupId>org.swinglabs</groupId>
       <artifactId>wizard</artifactId>
       <version>0.998.2-SNAPSHOT</version>

File src/main/java/org/nrg/net/RestServer.java

  * @author Kevin A. Archie <karchie@wustl.edu>
  *
  */
-public final class RestServer {
+public class RestServer {
 	private static final Pattern userInfoPattern = Pattern.compile("([^:@/]*):([^:@]*)");
 	private static final String GET = "GET", PUT = "PUT", POST = "POST";
 	private static final String AUTHORIZATION_HEADER = "Authorization";

File src/main/java/org/nrg/upload/data/SubjectInformation.java

+package org.nrg.upload.data;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.nrg.net.RestServer;
+import org.nrg.net.StringResponseProcessor;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+public class SubjectInformation {
+	private static final String MIME_TYPE = "text/xml";
+	private final RestServer xnat;
+	private final Project project;
+	private final String uri;
+
+	private String label;
+
+	public SubjectInformation(RestServer xnat, Project project) {
+		this.xnat = xnat;
+		this.project = project;
+
+		final StringBuilder sb = new StringBuilder("/REST/projects/");
+		sb.append(project.toString());
+		sb.append("/subjects");
+		uri = sb.toString();
+	}
+
+	public Subject uploadTo() throws UploadSubjectException {
+		final StringResponseProcessor processor = createProcessor();
+		try {
+			xnat.doPost(uri, processor);
+		} catch (Exception e) {
+			throw new UploadSubjectException("Error submitting new subject XML to XNAT.", e);
+		}
+
+		// parse out id from response
+		return new Subject(label, parseId(processor.toString()));
+	}
+
+	public void setLabel(String label) {
+		this.label = label;
+	}
+
+	public String getLabel() {
+		return label;
+	}
+
+	public String toString() {
+		return getLabel();
+	}
+
+	protected Document buildXML() throws ParserConfigurationException {
+		final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+		final Document document = builder.getDOMImplementation().createDocument("http://nrg.wustl.edu/xnat", "xnat:Subject", null);
+
+		final Element root = document.getDocumentElement();
+		root.setAttribute("project", project.toString());
+		root.setAttribute("label", label);
+		return document;
+	}
+
+	private StringResponseProcessor createProcessor() throws UploadSubjectException {
+		try {
+			final InputStream xmlStream = transformXML(buildXML());
+			final StringResponseProcessor processor = new StringResponseProcessor(xmlStream, MIME_TYPE, null, null);
+			return processor;
+		} catch (Exception e) {
+			throw new UploadSubjectException("Error generating XML to create new subject.", e);
+		}
+	}
+
+	private InputStream transformXML(Document document) throws TransformerConfigurationException, TransformerException {
+		// temporary buffer
+		final ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+		// perform the transform
+		final DOMSource source = new DOMSource(document);
+		final StreamResult result = new StreamResult(out);
+		final Transformer transformer = TransformerFactory.newInstance().newTransformer();
+		transformer.transform(source, result);
+
+		// convert OutputStream to InputStream (could use
+		// Piped{Input,Output}Stream, but would require spawning a thread, plus
+		// we should have enough memory for the subject XML document)
+		return new ByteArrayInputStream(out.toByteArray());
+	}
+
+	private String parseId(String response) {
+		// response should contain a URI to the newly created subject, we take
+		// the last part of that URI as the subject's ID
+		String[] parts = response.trim().split("/");
+		return parts[parts.length - 1];
+	}
+
+	public static final class UploadSubjectException extends Exception {
+		private static final long serialVersionUID = -1331357997499624104L;
+
+		public UploadSubjectException(String message, Throwable e) {
+			super(message, e);
+		}
+	}
+}

File src/main/java/org/nrg/upload/ui/Invoker.java

 		final Dimension dimension = new Dimension(640, 380);
 		final WizardPage[] pages = new WizardPage[] {
 				new SelectProjectPage(xnat, new URL(System.getProperty("scp.url")), dimension),
-				new SelectSubjectPage(dimension),
+				new SelectSubjectPage(xnat, dimension),
 				new SelectFilesPage(),
 				new SelectSessionPage(),
 				new AssignSessionVariablesPage(),

File src/main/java/org/nrg/upload/ui/NewSubjectController.java

+package org.nrg.upload.ui;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import org.nrg.net.RestServer;
+import org.nrg.upload.data.Project;
+import org.nrg.upload.data.Subject;
+import org.nrg.upload.data.SubjectInformation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class NewSubjectController implements ActionListener {
+	private final Logger logger = LoggerFactory.getLogger(NewSubjectController.class);
+
+	private final NewSubjectDialog dialog;
+	private final SelectSubjectPage subjectPage;
+	private final RestServer xnat;
+	private final Project project;
+
+
+	public NewSubjectController(final NewSubjectDialog dialog, final SelectSubjectPage subjectPage, final RestServer xnat,
+			final Project project) {
+		this.dialog = dialog;
+		this.subjectPage = subjectPage;
+		this.xnat = xnat;
+		this.project = project;
+	}
+
+	public void actionPerformed(ActionEvent e) {
+		if (validate()) {
+			try {
+				Subject subject = buildModel().uploadTo();
+				dialog.close();
+			} catch (SubjectInformation.UploadSubjectException ex) {
+				logger.error("Issue uploading new subject XML to XNAT.", ex);
+				dialog.error(ex.getMessage());
+			}
+		}
+	}
+
+	SubjectInformation buildModel() {
+		SubjectInformation model = new SubjectInformation(xnat, project);
+		model.setLabel(dialog.getLabelValue());
+		return model;
+	}
+
+	private boolean validate() {
+		if (!completed(dialog.getLabelValue())) {
+			dialog.error("Label can not be empty");
+			return false;
+		}
+		return true;
+	}
+
+	private boolean completed(String value) {
+		return value != null && value.trim().equals("");
+	}
+
+}

File src/main/java/org/nrg/upload/ui/NewSubjectDialog.java

+package org.nrg.upload.ui;
+
+import java.awt.GridBagLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+import org.nrg.net.RestServer;
+import org.nrg.upload.data.Project;
+
+public class NewSubjectDialog extends JDialog {
+	private static final long serialVersionUID = -4915887756173576128L;
+
+	private final NewSubjectController controller;
+
+	private JTextField label;
+	
+	public NewSubjectDialog(final SelectSubjectPage subjectPage,final RestServer xnat, final Project project) {
+		super();
+
+		controller = new NewSubjectController(this, subjectPage, xnat, project);
+
+		setModal(true);
+		getContentPane().add(makePanel());
+		pack();
+	}
+
+	private JPanel makePanel() {
+		final JPanel panel = new JPanel();
+		panel.setLayout(new GridBagLayout());
+
+		label = new JTextField(16);
+		panel.add(label);
+
+		panel.add(newButton("Create Subject", controller));
+
+		panel.add(newButton("Cancel", new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				close();
+			}
+		}));
+
+		return panel;
+	}
+
+	private JButton newButton(String label, ActionListener listener) {
+		JButton button = new JButton(label);
+		button.addActionListener(listener);
+		return button;
+	}
+
+	public void error(String message) {
+		JOptionPane.showMessageDialog(this, message, "New Subject Error", JOptionPane.ERROR_MESSAGE);
+	}
+	
+	public void close() {
+		setVisible(false);
+	}
+
+	String getLabelValue(){
+		return label.getText();
+	}
+}

File src/main/java/org/nrg/upload/ui/SelectSubjectPage.java

 
 import java.awt.Component;
 import java.awt.Dimension;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
 import java.util.concurrent.ExecutionException;
 
 import javax.swing.DefaultListModel;
+import javax.swing.JButton;
+import javax.swing.JDialog;
 import javax.swing.JList;
 import javax.swing.JScrollPane;
 
 import org.netbeans.spi.wizard.WizardPage;
+import org.nrg.net.RestServer;
 import org.nrg.upload.data.Project;
 import org.nrg.upload.data.Subject;
 
 	private final JList list;
 	private final DefaultListModel listModel = new DefaultListModel();
 	private final Dimension dimension;
+	private final RestServer xnat;
 	
 	public static final String getDescription() {
 		return STEP_DESCRIPTION;
 	}
 	
 
-	public SelectSubjectPage(final Dimension dimension) {
+	public SelectSubjectPage(final RestServer xnat, final Dimension dimension) {
+		this.xnat = xnat;
 		this.dimension = dimension;
 		list = new JList(listModel);
 		list.setName(PRODUCT_NAME);
 		setLongDescription(LONG_DESCRIPTION);
 	}
-	
-	public SelectSubjectPage() {
-		this(null);
-	}
-		
+
 	/*
 	 * (non-Javadoc)
 	 * @see org.netbeans.spi.wizard.WizardPage#recycle()
 				listModel.removeAllElements();
 				try {
 					for (final Subject subject : project.getSubjects()) {
-						listModel.addElement(subject);
+						addSubject(subject);
 					}
 					break SUBJECT_LABELS;
 				} catch (InterruptedException retry) {
 					} catch (InterruptedException ignore) {}
 				}
 			}
-			if (listModel.contains(prevSelection)) {
-				list.setSelectedValue(prevSelection, true);
-			}
+			selectSubject(prevSelection);
+			
+			final JButton newSubject = new JButton("New Subject");
+			newSubject.addActionListener(new ActionListener() {
+				public void actionPerformed(ActionEvent e) {
+					JDialog dialog = new NewSubjectDialog(this, xnat, project);
+					dialog.setVisible(true);
+				}
+			});
+			add(newSubject);
 		}
 		setBusy(false);
 	}
 	protected String validateContents(final Component component, final Object o) {
 		return null == getWizardData(PRODUCT_NAME) ? "Select a subject" : null;
 	}
+	
+	void selectSubject(Object selection){
+		if (listModel.contains(selection)) {
+			list.setSelectedValue(selection, true);
+		}
+	}
+	
+	void addSubject(Subject subject){
+		listModel.addElement(subject);
+	}
 }

File src/main/java/org/nrg/upload/ui/UploadAssistantApplet.java

 			logger.trace(this + " initializing pages");
 			final WizardPage project = new SelectProjectPage(xnat, new URL(getParameter(SCP_WS_URL)), dimension);
 			logger.trace("project");
-			final WizardPage subject = new SelectSubjectPage(dimension);
+			final WizardPage subject = new SelectSubjectPage(xnat, dimension);
 			logger.trace("subject");
 			final WizardPage files = new SelectFilesPage();
 			logger.trace("files");

File src/test/java/org/nrg/upload/data/SubjectInformationTest.java

+package org.nrg.upload.data;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.net.HttpURLConnection;
+
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.nrg.net.HttpURLConnectionProcessor;
+import org.nrg.net.RestServer;
+import org.nrg.net.StringResponseProcessor;
+import org.w3c.dom.Document;
+
+public class SubjectInformationTest {
+
+	// private final Logger logger =
+	// LoggerFactory.getLogger(SubjectInformationTest.class);
+	private static final String EXPECTED_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>"
+			+ "<xnat:Subject label=\"SUBJ01_MR1\" project=\"PROJ01\" xmlns:xnat=\"http://nrg.wustl.edu/xnat\"/>";
+
+	private SubjectInformation info;
+	private Project project;
+	private RestServer xnat;
+
+	@Before
+	public void setUp() throws Exception {
+		project = new Project("PROJ01", null, null);
+
+		xnat = mock(RestServer.class);
+
+		info = new SubjectInformation(xnat, project);
+		info.setLabel("SUBJ01_MR1");
+	}
+
+	@Test
+	public void shouldCreateXML() throws Exception {
+		String xml = xmlToString(info.buildXML());
+		assertEquals(EXPECTED_XML, xml);
+	}
+
+	@Test
+	public void shouldCreateSubjectOnServerAndReturnSubject() throws Exception {
+
+		mockHttpResponse("http://localhost:8080/xnat/REST/projects/PROJ01/subjects/Demo_S00003", EXPECTED_XML).doPost(
+				eq("/REST/projects/PROJ01/subjects"), any(HttpURLConnectionProcessor.class));
+
+		Subject subject = info.uploadTo();
+
+		// subject checks
+		assertEquals("SUBJ01_MR1", subject.getLabel());
+		assertEquals("Demo_S00003", subject.getId());
+	}
+
+	@Test(expected=SubjectInformation.UploadSubjectException.class)
+	public void shouldWrapException() throws Exception {
+		doThrow(new IOException()).when(xnat).doPost(anyString(), any(HttpURLConnectionProcessor.class));
+		
+		info.uploadTo();
+	}
+
+	private String xmlToString(Document document) throws Exception {
+		StringWriter writer = new StringWriter();
+
+		DOMSource source = new DOMSource(document);
+		StreamResult result = new StreamResult(writer);
+		try {
+			Transformer transformer = TransformerFactory.newInstance().newTransformer();
+			transformer.transform(source, result);
+		} catch (TransformerConfigurationException e) {
+			throw e;
+		} catch (TransformerException e) {
+			throw e;
+		}
+		return writer.toString();
+	}
+
+	/**
+	 * Using the mock RestServer, we inject "response" into the Response
+	 * Processor.
+	 */
+	private RestServer mockHttpResponse(final String response, final String expectedRequest) throws IOException {
+		return doAnswer(new Answer<Object>() {
+			public Object answer(InvocationOnMock invocation) throws IOException {
+				// create a mock connection that provides an InputStream seeded
+				// with the "response"
+				HttpURLConnection connection = mock(HttpURLConnection.class);
+				when(connection.getInputStream()).thenReturn(new ByteArrayInputStream(response.getBytes()));
+				ByteArrayOutputStream out = new ByteArrayOutputStream();
+				when(connection.getOutputStream()).thenReturn(out);
+
+				StringResponseProcessor processor = (StringResponseProcessor) invocation.getArguments()[1];
+				processor.prepare(connection);
+				processor.process(connection);
+
+				// ensure the data sent to the server matches what we expect
+				// TODO consider moving the assert out of the mocking code
+				if (expectedRequest != null) {
+					assertEquals(expectedRequest, out.toString());
+				}
+
+				return null;
+			}
+		}).when(xnat);
+	}
+}

File src/test/java/org/nrg/upload/ui/NewSubjectControllerTest.java

+package org.nrg.upload.ui;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.*;
+
+import java.awt.event.ActionEvent;
+import java.io.IOException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.nrg.net.HttpURLConnectionProcessor;
+import org.nrg.net.RestServer;
+import org.nrg.net.StringResponseProcessor;
+import org.nrg.upload.data.Project;
+import org.nrg.upload.data.SubjectInformation;
+
+public class NewSubjectControllerTest {
+
+	private NewSubjectDialog dialog;
+	private NewSubjectController controller;
+	private Project project;
+	private RestServer xnat;
+	private ActionEvent event;
+
+	@Before
+	public void setUp() throws Exception {
+		project = new Project("PROJ01", null, null);
+		dialog = mock(NewSubjectDialog.class);
+		xnat = mock(RestServer.class);
+		event = mock(ActionEvent.class);
+
+		controller = new NewSubjectController(dialog, xnat, project);
+	}
+
+	@Test
+	public void shouldMapFieldsFromDialogToModel() {
+		when(dialog.getLabelValue()).thenReturn("S01_MR1");
+
+		SubjectInformation model = controller.buildModel();
+		assertEquals("S01_MR1", model.getLabel());
+	}
+
+	@Test
+	public void shouldUploadModelToSiteOnEvent() throws Exception {
+		controller.actionPerformed(event);
+
+		verify(xnat).doPost(eq("/REST/projects/PROJ01/subjects"), any(StringResponseProcessor.class));
+		verify(dialog).close();
+	}
+
+	@Test
+	public void shouldDisplayErrors() throws Exception {
+		doThrow(new IOException()).when(xnat).doPost(anyString(), any(HttpURLConnectionProcessor.class));
+		controller.actionPerformed(event);
+
+		verify(dialog).error(eq("Error submitting new subject XML to XNAT."));
+		verify(dialog, never()).close();
+	}
+
+}