From 7417de767e54a6c162b757002ef4120931bcc84b Mon Sep 17 00:00:00 2001
From: Rick Herrick <jrherrick@wustl.edu>
Date: Thu, 1 Sep 2016 17:10:57 -0500
Subject: [PATCH] XNAT-4394 Updated investigators API to include projects on
 which investigator is PI or associate investigator.

---
 .../org/nrg/xapi/rest/data/Investigator.java  | 209 ++++++++++++++++++
 .../xapi/rest/data/InvestigatorService.java   |  48 ++++
 .../nrg/xapi/rest/data/InvestigatorsApi.java  |  80 ++-----
 3 files changed, 279 insertions(+), 58 deletions(-)
 create mode 100644 src/main/java/org/nrg/xapi/rest/data/Investigator.java
 create mode 100644 src/main/java/org/nrg/xapi/rest/data/InvestigatorService.java

diff --git a/src/main/java/org/nrg/xapi/rest/data/Investigator.java b/src/main/java/org/nrg/xapi/rest/data/Investigator.java
new file mode 100644
index 00000000..1a9ca3ac
--- /dev/null
+++ b/src/main/java/org/nrg/xapi/rest/data/Investigator.java
@@ -0,0 +1,209 @@
+package org.nrg.xapi.rest.data;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import org.apache.commons.lang3.NotImplementedException;
+import org.nrg.xdat.bean.XnatInvestigatordataBean;
+import org.nrg.xdat.model.XnatInvestigatordataI;
+import org.nrg.xdat.om.XnatInvestigatordata;
+
+import java.io.Writer;
+import java.sql.Array;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.*;
+
+/**
+ * Provides a wrapper around the {@link XnatInvestigatordata}, {@link XnatInvestigatordataI}, and {@link
+ * XnatInvestigatordataBean} classes. This omits extraneous properties (e.g. XFT integration attributes) and provides
+ * extra attributes for aggregated or synthesized properties, such as lists of projects with which the investigator is
+ * associated.
+ */
+public class Investigator implements XnatInvestigatordataI {
+    public Investigator(final XnatInvestigatordataI investigator, final Collection<String> primaryProjects, final Collection<String> investigatorProjects) {
+        _xnatInvestigatordataId = investigator.getXnatInvestigatordataId();
+        _id = investigator.getId();
+        _title = investigator.getTitle();
+        _firstname = investigator.getFirstname();
+        _lastname = investigator.getLastname();
+        _institution = investigator.getInstitution();
+        _department = investigator.getDepartment();
+        _email = investigator.getEmail();
+        _phone = investigator.getPhone();
+        _primaryProjects.addAll(primaryProjects);
+        _investigatorProjects.addAll(investigatorProjects);
+    }
+
+    public Investigator(final ResultSet resultSet) throws SQLException {
+        _xnatInvestigatordataId = resultSet.getInt(0);
+        _id = resultSet.getString(1);
+        _title = resultSet.getString(2);
+        _firstname = resultSet.getString(2);
+        _lastname = resultSet.getString(2);
+        _institution = resultSet.getString(2);
+        _department = resultSet.getString(2);
+        _email = resultSet.getString(2);
+        _phone = resultSet.getString(2);
+        _primaryProjects.addAll(getProjectIds(resultSet.getArray(10)));
+        _investigatorProjects.addAll(getProjectIds(resultSet.getArray(11)));
+    }
+
+    @Override
+    @JsonIgnore
+    public String getXSIType() {
+        return XnatInvestigatordata.SCHEMA_ELEMENT_NAME;
+    }
+
+    @Override
+    public void toXML(final Writer writer) throws Exception {
+
+    }
+
+    @Override
+    public String getTitle() {
+        return _title;
+    }
+
+    @Override
+    public void setTitle(final String title) {
+        throw new NotImplementedException("Set methods on this class are not implemented: the class is meant to be read-only and only for reference purposes.");
+    }
+
+    @Override
+    public String getFirstname() {
+        return _firstname;
+    }
+
+    @Override
+    public void setFirstname(final String firstname) {
+        throw new NotImplementedException("Set methods on this class are not implemented: the class is meant to be read-only and only for reference purposes.");
+    }
+
+    @Override
+    public String getLastname() {
+        return _lastname;
+    }
+
+    @Override
+    public void setLastname(final String lastname) {
+        throw new NotImplementedException("Set methods on this class are not implemented: the class is meant to be read-only and only for reference purposes.");
+    }
+
+    @Override
+    public String getInstitution() {
+        return _institution;
+    }
+
+    @Override
+    public void setInstitution(final String institution) {
+        throw new NotImplementedException("Set methods on this class are not implemented: the class is meant to be read-only and only for reference purposes.");
+    }
+
+    @Override
+    public String getDepartment() {
+        return _department;
+    }
+
+    @Override
+    public void setDepartment(final String department) {
+        throw new NotImplementedException("Set methods on this class are not implemented: the class is meant to be read-only and only for reference purposes.");
+    }
+
+    @Override
+    public String getEmail() {
+        return _email;
+    }
+
+    @Override
+    public void setEmail(final String email) {
+        throw new NotImplementedException("Set methods on this class are not implemented: the class is meant to be read-only and only for reference purposes.");
+    }
+
+    @Override
+    public String getPhone() {
+        return _phone;
+    }
+
+    @Override
+    public void setPhone(final String phone) {
+        throw new NotImplementedException("Set methods on this class are not implemented: the class is meant to be read-only and only for reference purposes.");
+    }
+
+    @Override
+    public String getId() {
+        return _id;
+    }
+
+    @Override
+    public void setId(final String v) {
+        throw new NotImplementedException("Set methods on this class are not implemented: the class is meant to be read-only and only for reference purposes.");
+    }
+
+    @Override
+    public Integer getXnatInvestigatordataId() {
+        return _xnatInvestigatordataId;
+    }
+
+    /**
+     * Gets the list of IDs for projects on which the investigator is the primary investigator.
+     *
+     * @return The projects on which the investigator is the primary investigator.
+     */
+    @SuppressWarnings("unused")
+    public Set<String> getPrimaryProjects() {
+        return new HashSet<>(_primaryProjects);
+    }
+
+    /**
+     * Indicates whether the investigator is a PI on the indicated project.
+     *
+     * @param project The project to test.
+     *
+     * @return Returns true if the investigator is the PI on the indicated project, false otherwise.
+     */
+    @SuppressWarnings("unused")
+    public boolean hasPrimaryProject(final String project) {
+        return _primaryProjects.contains(project);
+    }
+
+    /**
+     * Gets the list of IDs for projects on which the investigator is the primary investigator.
+     *
+     * @return The projects on which the investigator is the primary investigator.
+     */
+    @SuppressWarnings("unused")
+    public Set<String> getInvestigatorProjects() {
+        return new HashSet<>(_investigatorProjects);
+    }
+
+    /**
+     * Indicates whether the investigator is a member of the indicated project team. Note that this does not check
+     * whether the investigator is the project's PI.
+     *
+     * @param project The project to test.
+     *
+     * @return Returns true if the investigator is a member of the indicated project team, false otherwise.
+     */
+    @SuppressWarnings("unused")
+    public boolean hasInvestigatorProject(final String project) {
+        return _investigatorProjects.contains(project);
+    }
+
+    private static Collection<? extends String> getProjectIds(final Array array) throws SQLException {
+        if (array == null) {
+            return Collections.emptyList();
+        }
+        return Arrays.asList((String[]) array.getArray());
+    }
+
+    private final Integer _xnatInvestigatordataId;
+    private final String _id;
+    private final String _title;
+    private final String _firstname;
+    private final String _lastname;
+    private final String _institution;
+    private final String _department;
+    private final String _email;
+    private final String _phone;
+    private final Set<String> _primaryProjects      = new HashSet<>();
+    private final Set<String> _investigatorProjects = new HashSet<>();
+}
diff --git a/src/main/java/org/nrg/xapi/rest/data/InvestigatorService.java b/src/main/java/org/nrg/xapi/rest/data/InvestigatorService.java
new file mode 100644
index 00000000..ad6e1a88
--- /dev/null
+++ b/src/main/java/org/nrg/xapi/rest/data/InvestigatorService.java
@@ -0,0 +1,48 @@
+package org.nrg.xapi.rest.data;
+
+import org.nrg.xdat.model.XnatInvestigatordataI;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.stereotype.Service;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+/**
+ * Manages operations with {@link Investigator investigator proxy objects}. This is not a full-on Hibernate service,
+ * since the "entities" managed are not Hibernate entities but instead are composite objects that represent XFT {@link
+ * XnatInvestigatordataI} objects as well as metadata aggregated from other tables.
+ */
+@Service
+public class InvestigatorService {
+    @Autowired
+    public InvestigatorService(final JdbcTemplate template) {
+        _template = template;
+    }
+
+    public Investigator getInvestigator(final int xnatInvestigatordataId) {
+        return _template.queryForObject(INVESTIGATOR_QUERY + BY_ID_WHERE, new Object[]{xnatInvestigatordataId}, ROW_MAPPER);
+    }
+
+    public Investigator getInvestigator(final String firstname, final String lastname) {
+        return _template.queryForObject(INVESTIGATOR_QUERY + BY_FIRST_LAST_WHERE, new Object[]{firstname, lastname}, ROW_MAPPER);
+    }
+
+    public List<Investigator> getInvestigators() {
+        return _template.query(INVESTIGATOR_QUERY, ROW_MAPPER);
+    }
+
+    private static final String INVESTIGATOR_QUERY  = "SELECT investigator.xnat_investigatordata_id AS xnat_investigatordata_id, investigator.id AS id, investigator.title AS title, investigator.firstname AS firstname, investigator.lastname AS lastname, investigator.institution AS institution, investigator.department AS department, investigator.email AS email, investigator.phone AS phone, (SELECT array(SELECT project.id FROM xnat_projectdata project WHERE project.pi_xnat_investigatordata_id = investigator.xnat_investigatordata_id)) AS primary_inv, (SELECT array(SELECT project_inv.xnat_projectdata_id FROM xnat_projectdata_investigator project_inv WHERE project_inv.xnat_investigatordata_xnat_investigatordata_id = investigator.xnat_investigatordata_id)) AS inv FROM xnat_investigatordata investigator";
+    private static final String BY_ID_WHERE         = " WHERE investigator.xnat_investigatordata_id = ?";
+    private static final String BY_FIRST_LAST_WHERE = " WHERE investigator.firstname = ? AND investigator.lastname = ?";
+
+    private static final RowMapper<Investigator> ROW_MAPPER = new RowMapper<Investigator>() {
+        @Override
+        public Investigator mapRow(final ResultSet resultSet, final int i) throws SQLException {
+            return new Investigator(resultSet);
+        }
+    };
+    private final JdbcTemplate _template;
+}
diff --git a/src/main/java/org/nrg/xapi/rest/data/InvestigatorsApi.java b/src/main/java/org/nrg/xapi/rest/data/InvestigatorsApi.java
index db058218..be3b56eb 100644
--- a/src/main/java/org/nrg/xapi/rest/data/InvestigatorsApi.java
+++ b/src/main/java/org/nrg/xapi/rest/data/InvestigatorsApi.java
@@ -6,11 +6,11 @@ import io.swagger.annotations.ApiResponse;
 import io.swagger.annotations.ApiResponses;
 import org.apache.commons.lang3.StringUtils;
 import org.nrg.framework.annotations.XapiRestController;
-import org.nrg.xdat.bean.XnatInvestigatordataBean;
-import org.nrg.xdat.model.XnatInvestigatordataI;
 import org.nrg.xdat.om.XnatInvestigatordata;
 import org.nrg.xdat.rest.AbstractXapiRestController;
+import org.nrg.xdat.security.helpers.Roles;
 import org.nrg.xdat.security.services.RoleHolder;
+import org.nrg.xdat.security.services.RoleRepositoryServiceI;
 import org.nrg.xdat.security.services.UserManagementServiceI;
 import org.nrg.xft.XFTItem;
 import org.nrg.xft.event.EventUtils;
@@ -22,12 +22,8 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
-import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.jdbc.core.RowMapper;
 import org.springframework.web.bind.annotation.*;
 
-import java.sql.ResultSet;
-import java.sql.SQLException;
 import java.util.List;
 
 @Api(description = "XNAT Data Investigators API")
@@ -35,42 +31,42 @@ import java.util.List;
 @RequestMapping(value = "/investigators")
 public class InvestigatorsApi extends AbstractXapiRestController {
     @Autowired
-    public InvestigatorsApi(final UserManagementServiceI userManagementService, final RoleHolder roleHolder, final JdbcTemplate jdbcTemplate) {
+    public InvestigatorsApi(final UserManagementServiceI userManagementService, final RoleHolder roleHolder, final InvestigatorService service) {
         super(userManagementService, roleHolder);
-        _jdbcTemplate = jdbcTemplate;
+        _service = service;
     }
 
-    @ApiOperation(value = "Get list of investigators.", notes = "The investigators function returns a list of all investigators configured in the XNAT system.", response = XnatInvestigatordataI.class, responseContainer = "List")
+    @ApiOperation(value = "Get list of investigators.", notes = "The investigators function returns a list of all investigators configured in the XNAT system.", response = Investigator.class, responseContainer = "List")
     @ApiResponses({@ApiResponse(code = 200, message = "Returns a list of all of the currently configured investigators."),
                    @ApiResponse(code = 500, message = "An unexpected or unknown error occurred")})
     @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET)
     @ResponseBody
-    public ResponseEntity<List<XnatInvestigatordataI>> getInvestigators() {
-        return new ResponseEntity<>(_jdbcTemplate.query(PI_QUERY + PI_ORDERBY, PI_ROWMAPPER), HttpStatus.OK);
+    public ResponseEntity<List<Investigator>> getInvestigators() {
+        return new ResponseEntity<>(_service.getInvestigators(), HttpStatus.OK);
     }
 
-    @ApiOperation(value = "Gets the requested investigator.", notes = "Returns the investigator with the specified ID.", response = XnatInvestigatordataI.class)
+    @ApiOperation(value = "Gets the requested investigator.", notes = "Returns the investigator with the specified ID.", response = Investigator.class)
     @ApiResponses({@ApiResponse(code = 200, message = "Returns the requested investigator."),
                    @ApiResponse(code = 404, message = "The requested investigator wasn't found."),
                    @ApiResponse(code = 500, message = "An unexpected or unknown error occurred.")})
     @RequestMapping(value = "{investigatorId}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET)
     @ResponseBody
-    public ResponseEntity<XnatInvestigatordataI> getInvestigator(@PathVariable("investigatorId") final int investigatorId) {
-        final XnatInvestigatordataI investigator = getXnatInvestigator(investigatorId);
+    public ResponseEntity<Investigator> getInvestigator(@PathVariable("investigatorId") final int investigatorId) {
+        final Investigator investigator = _service.getInvestigator(investigatorId);
         if (investigator == null) {
             return new ResponseEntity<>(HttpStatus.NOT_FOUND);
         }
         return new ResponseEntity<>(investigator, HttpStatus.OK);
     }
 
-    @ApiOperation(value = "Creates a new investigator from the submitted attributes.", notes = "Returns the newly created investigator with the submitted attributes.", response = XnatInvestigatordataI.class)
+    @ApiOperation(value = "Creates a new investigator from the submitted attributes.", notes = "Returns the newly created investigator with the submitted attributes.", response = Investigator.class)
     @ApiResponses({@ApiResponse(code = 200, message = "Returns the newly created investigator."),
                    @ApiResponse(code = 403, message = "Insufficient privileges to create the submitted investigator."),
                    @ApiResponse(code = 404, message = "The requested investigator wasn't found."),
                    @ApiResponse(code = 500, message = "An unexpected or unknown error occurred.")})
     @RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.POST)
     @ResponseBody
-    public ResponseEntity<XnatInvestigatordataI> createInvestigator(@RequestBody final XnatInvestigatordataBean investigator) throws Exception {
+    public ResponseEntity<Investigator> createInvestigator(@RequestBody final Investigator investigator) throws Exception {
         if (StringUtils.isBlank(investigator.getFirstname()) || StringUtils.isBlank(investigator.getLastname())) {
             _log.error("User {} tried to create investigator without a first or last name.", getSessionUser().getUsername());
             return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
@@ -88,10 +84,10 @@ public class InvestigatorsApi extends AbstractXapiRestController {
             _log.error("Failed to create a new investigator for user {}. Check the logs for possible errors or exceptions.", user.getUsername());
             return new ResponseEntity<>(HttpStatus.CONFLICT);
         }
-        return new ResponseEntity<>(getXnatInvestigator(investigator.getFirstname(), investigator.getLastname()), HttpStatus.OK);
+        return new ResponseEntity<>(_service.getInvestigator(investigator.getFirstname(), investigator.getLastname()), HttpStatus.OK);
     }
 
-    @ApiOperation(value = "Updates the requested investigator from the submitted attributes.", notes = "Returns the updated investigator.", response = XnatInvestigatordataI.class)
+    @ApiOperation(value = "Updates the requested investigator from the submitted attributes.", notes = "Returns the updated investigator.", response = Investigator.class)
     @ApiResponses({@ApiResponse(code = 200, message = "Returns the updated investigator."),
                    @ApiResponse(code = 304, message = "The requested investigator is the same as the submitted investigator."),
                    @ApiResponse(code = 403, message = "Insufficient privileges to edit the requested investigator."),
@@ -99,8 +95,9 @@ public class InvestigatorsApi extends AbstractXapiRestController {
                    @ApiResponse(code = 500, message = "An unexpected or unknown error occurred.")})
     @RequestMapping(value = "{investigatorId}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.PUT)
     @ResponseBody
-    public ResponseEntity<XnatInvestigatordataI> updateInvestigator(@PathVariable("investigatorId") final int investigatorId, @RequestBody final XnatInvestigatordataBean investigator) throws Exception {
+    public ResponseEntity<Investigator> updateInvestigator(@PathVariable("investigatorId") final int investigatorId, @RequestBody final Investigator investigator) throws Exception {
         final UserI user = getSessionUser();
+
         final XnatInvestigatordata existing = XnatInvestigatordata.getXnatInvestigatordatasByXnatInvestigatordataId(investigatorId, user, false);
         if (existing == null) {
             return new ResponseEntity<>(HttpStatus.NOT_FOUND);
@@ -142,7 +139,7 @@ public class InvestigatorsApi extends AbstractXapiRestController {
                 _log.error("Failed to save the investigator with ID {}. Check the logs for possible errors or exceptions.");
                 return new ResponseEntity<>(HttpStatus.CONFLICT);
             }
-            return new ResponseEntity<>(getXnatInvestigator(investigatorId), HttpStatus.OK);
+            return new ResponseEntity<>(_service.getInvestigator(investigatorId), HttpStatus.OK);
         }
 
         return new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
@@ -150,12 +147,16 @@ public class InvestigatorsApi extends AbstractXapiRestController {
 
     @ApiOperation(value = "Deletes the requested investigator.", notes = "Returns true if the requested investigator was successfully deleted. Returns false otherwise.", response = Boolean.class)
     @ApiResponses({@ApiResponse(code = 200, message = "Returns true to indicate the requested investigator was successfully deleted."),
+                   @ApiResponse(code = 403, message = "The user doesn't have permission to delete investigators."),
                    @ApiResponse(code = 404, message = "The requested investigator wasn't found."),
                    @ApiResponse(code = 500, message = "An unexpected or unknown error occurred.")})
     @RequestMapping(value = "{investigatorId}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.DELETE)
     @ResponseBody
     public ResponseEntity<Boolean> deleteInvestigator(@PathVariable("investigatorId") final int investigatorId) throws Exception {
         final UserI user = getSessionUser();
+        if (!Roles.isSiteAdmin(user)) {
+            return new ResponseEntity<>(HttpStatus.FORBIDDEN);
+        }
         final XnatInvestigatordata investigator = XnatInvestigatordata.getXnatInvestigatordatasByXnatInvestigatordataId(investigatorId, user, false);
         if (investigator == null) {
             return new ResponseEntity<>(false, HttpStatus.NOT_FOUND);
@@ -164,44 +165,7 @@ public class InvestigatorsApi extends AbstractXapiRestController {
         return new ResponseEntity<>(true, HttpStatus.OK);
     }
 
-    private XnatInvestigatordataI getXnatInvestigator(final int investigatorId) {
-        final List<XnatInvestigatordataI> investigators = _jdbcTemplate.query(PI_QUERY + PI_BY_ID, new Object[]{investigatorId}, PI_ROWMAPPER);
-        if (investigators.size() == 0) {
-            return null;
-        }
-        return investigators.get(0);
-    }
-
-    private XnatInvestigatordataI getXnatInvestigator(final String firstname, final String lastname) {
-        final List<XnatInvestigatordataI> investigators = _jdbcTemplate.query(PI_QUERY + PI_BY_NAME, new Object[]{firstname, lastname}, PI_ROWMAPPER);
-        if (investigators.size() == 0) {
-            return null;
-        }
-        return investigators.get(0);
-    }
-
     private static final Logger _log = LoggerFactory.getLogger(InvestigatorsApi.class);
 
-    // TODO: InvestigatorListResource uses a query that adds the PI's login. XnatInvestigatordataBean has no setter for login, so leaving it off for now.
-    private static final String                           PI_QUERY     = "SELECT DISTINCT ON (lastname, firstname) xnat_investigatorData_id, title, firstname, lastname, institution, department, email, phone FROM xnat_investigatorData ";
-    private static final String                           PI_ORDERBY   = "ORDER BY lastname, firstname";
-    private static final String                           PI_BY_ID     = "WHERE xnat_investigatordata_id = ?";
-    private static final String                           PI_BY_NAME   = "WHERE firstname = ? AND lastname = ?";
-    private final static RowMapper<XnatInvestigatordataI> PI_ROWMAPPER = new RowMapper<XnatInvestigatordataI>() {
-        @Override
-        public XnatInvestigatordataI mapRow(final ResultSet resultSet, final int i) throws SQLException {
-            return new XnatInvestigatordataBean() {{
-                setXnatInvestigatordataId(resultSet.getInt("xnat_investigatordata_id"));
-                setTitle(resultSet.getString("title"));
-                setFirstname(resultSet.getString("firstname"));
-                setLastname(resultSet.getString("lastname"));
-                setDepartment(resultSet.getString("department"));
-                setInstitution(resultSet.getString("institution"));
-                setEmail(resultSet.getString("email"));
-                setPhone(resultSet.getString("phone"));
-            }};
-        }
-    };
-
-    private final JdbcTemplate _jdbcTemplate;
+    private final InvestigatorService _service;
 }
-- 
GitLab