Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

7767 unable to download large guestbooks #7931

Merged
merged 21 commits into from Jun 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4a494fe
Merge branch 'develop' into 7767-unable-to-download-large-guestbooks
sekmiller May 19, 2021
e088aa6
Merge branch 'develop' into 7767-unable-to-download-large-guestbooks
sekmiller May 20, 2021
4a7d85b
Merge branch 'develop' into 7767-unable-to-download-large-guestbooks
sekmiller May 25, 2021
8905d8d
#7767 download guestbook responses api
sekmiller May 27, 2021
3ae3948
Merge branch 'develop' into 7767-unable-to-download-large-guestbooks
sekmiller May 27, 2021
2e3f698
#7767 code cleanup
sekmiller Jun 2, 2021
8deeab8
Merge branch 'develop' into 7767-unable-to-download-large-guestbooks
sekmiller Jun 2, 2021
0964747
Merge branch 'develop' into 7767-unable-to-download-large-guestbooks
sekmiller Jun 2, 2021
bf63db9
Merge branch 'develop' into 7767-unable-to-download-large-guestbooks
sekmiller Jun 2, 2021
e888ed8
#7767 add "OrDie" to find dv
sekmiller Jun 2, 2021
1ca6505
#7767 add perms to download gb api
sekmiller Jun 3, 2021
e4d311b
Merge branch 'develop' into 7767-unable-to-download-large-guestbooks
sekmiller Jun 4, 2021
e711fb3
#7767 update response building
sekmiller Jun 8, 2021
dc5e29e
#7767 add timestamp to file name
sekmiller Jun 8, 2021
6288b5e
Add docs for GB response api
sekmiller Jun 8, 2021
6e7d2db
Put in export for GUESTBOOK_ID
sekmiller Jun 8, 2021
5186396
typo(copy/paste-o)
sekmiller Jun 8, 2021
480cf21
Merge branch 'develop' into 7767-unable-to-download-large-guestbooks
sekmiller Jun 14, 2021
ea2545c
update CURL command for error response
sekmiller Jun 21, 2021
593b3d6
#7767 sort responses for gb download
sekmiller Jun 23, 2021
e7f26de
adding clarification to docs
sekmiller Jun 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 23 additions & 1 deletion doc/sphinx-guides/source/api/native-api.rst
Expand Up @@ -580,10 +580,32 @@ The fully expanded example above (without environment variables) looks like this

.. code-block:: bash

curl -H X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -X POST https://demo.dataverse.org/api/dataverses/root/actions/:publish
curl -H X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -X POST https://demo.dataverse.org/api/dataverses/root/actions/:publish

You should expect a 200 ("OK") response and JSON output.

Retrieve Guestbook Responses for a Dataverse Collection
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

In order to retrieve a file containing a list of Guestbook Responses in csv format for Dataverse collection, you must know either its "alias" (which the GUI calls an "identifier") or its database ID. If the Dataverse collection has more than one guestbook you may provide the id of a single guestbook as an optional parameter. If no guestbook id is provided the results returned will be the same as pressing the "Download All Responses" button on the Manage Dataset Guestbook page. If the guestbook id is provided then only those responses from that guestbook will be included in the file.

.. note:: See :ref:`curl-examples-and-environment-variables` if you are unfamiliar with the use of ``export`` below.

.. code-block:: bash

export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export SERVER_URL=https://demo.dataverse.org
export ID=root
export GUESTBOOK_ID=1

curl -O -J -f -H X-Dataverse-key:$API_TOKEN $SERVER_URL/api/dataverses/$ID/guestbookResponses?guestbookId=$GUESTBOOK_ID

The fully expanded example above (without environment variables) looks like this:

.. code-block:: bash

curl -O -J -f -H X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx https://demo.dataverse.org/api/dataverses/root/guestbookResponses?guestbookId=1

Datasets
--------

Expand Down
Expand Up @@ -128,6 +128,18 @@ public void streamResponsesByDataverseIdAndGuestbookId(OutputStream out, Long da
// of queries now) -- L.A.

Map<Integer, Object> customQandAs = mapCustomQuestionAnswersAsStrings(dataverseId, guestbookId);

List<Object[]> guestbookResults = getGuestbookResults( dataverseId, guestbookId );
// the CSV header:
out.write("Guestbook, Dataset, Dataset PID, Date, Type, File Name, File Id, File PID, User Name, Email, Institution, Position, Custom Questions\n".getBytes());
for (Object[] result : guestbookResults) {
StringBuilder sb = convertGuestbookResponsesToCSV(customQandAs, result);
out.write(sb.toString().getBytes());
out.flush();
}
}

public List<Object[]> getGuestbookResults(Long dataverseId, Long guestbookId ){

String queryString = BASE_QUERY_STRING_FOR_DOWNLOAD_AS_CSV
+ " and o.owner_id = "
Expand All @@ -137,15 +149,15 @@ public void streamResponsesByDataverseIdAndGuestbookId(OutputStream out, Long da
queryString+= (" and r.guestbook_id = " + guestbookId.toString());
}

queryString += ";";
queryString += " ORDER by r.id DESC;";
logger.fine("stream responses query: " + queryString);

List<Object[]> guestbookResults = em.createNativeQuery(queryString).getResultList();

// the CSV header:
out.write("Guestbook, Dataset, Dataset PID, Date, Type, File Name, File Id, File PID, User Name, Email, Institution, Position, Custom Questions\n".getBytes());
return em.createNativeQuery(queryString).getResultList();

for (Object[] result : guestbookResults) {
}

public StringBuilder convertGuestbookResponsesToCSV ( Map<Integer, Object> customQandAs, Object[] result) throws IOException {

Integer guestbookResponseId = (Integer)result[0];

StringBuilder sb = new StringBuilder();
Expand Down Expand Up @@ -208,36 +220,17 @@ public void streamResponsesByDataverseIdAndGuestbookId(OutputStream out, Long da

// Finally, custom questions and answers, if present:

// (the old implementation, below, would run one extra query FOR EVERY SINGLE
// guestbookresponse entry! -- instead, we are now pre-caching all the
// available custom question responses, with a single native query at
// the top of this method. -- L.A.)

/*String cqString = "select q.questionstring, r.response from customquestionresponse r, customquestion q where q.id = r.customquestion_id and r.guestbookResponse_id = " + result[0];
List<Object[]> customResponses = em.createNativeQuery(cqString).getResultList();
if (customResponses != null) {
for (Object[] response : customResponses) {
sb.append(SEPARATOR);
sb.append(response[0]);
sb.append(SEPARATOR);
sb.append(response[1] == null ? "" : response[1]);
}
}*/

if (customQandAs.containsKey(guestbookResponseId)) {
sb.append(customQandAs.get(guestbookResponseId));
}

sb.append(NEWLINE);

// Finally, write the line out:
// (i.e., we are writing one guestbook response at a time, thus allowing the
// whole thing to stream in real time -- L.A.)
out.write(sb.toString().getBytes());
out.flush();
}
return sb;

}


private String formatPersistentIdentifier(String protocol, String authority, String identifier) {
// Note that the persistent id may be unavailable for this dvObject:
if (StringUtil.nonEmpty(protocol) && StringUtil.nonEmpty(authority) && StringUtil.nonEmpty(identifier)) {
Expand Down Expand Up @@ -349,7 +342,7 @@ private Map<Integer, Object> mapCustomQuestionAnswersAsLists(Long dataverseId, L
return selectCustomQuestionAnswers(dataverseId, guestbookId, false, firstResponse, lastResponse);
}

private Map<Integer, Object> mapCustomQuestionAnswersAsStrings(Long dataverseId, Long guestbookId) {
public Map<Integer, Object> mapCustomQuestionAnswersAsStrings(Long dataverseId, Long guestbookId) {
return selectCustomQuestionAnswers(dataverseId, guestbookId, true, null, null);
}

Expand Down
57 changes: 57 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java
Expand Up @@ -11,6 +11,8 @@
import edu.harvard.iq.dataverse.authorization.DataverseRole;
import edu.harvard.iq.dataverse.DvObject;
import edu.harvard.iq.dataverse.GlobalId;
import edu.harvard.iq.dataverse.GuestbookResponseServiceBean;
import edu.harvard.iq.dataverse.GuestbookServiceBean;
import edu.harvard.iq.dataverse.MetadataBlock;
import edu.harvard.iq.dataverse.RoleAssignment;
import static edu.harvard.iq.dataverse.api.AbstractApiBean.error;
Expand Down Expand Up @@ -98,10 +100,16 @@
import javax.ws.rs.core.Response.Status;
import static edu.harvard.iq.dataverse.util.json.JsonPrinter.toJsonArray;
import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json;
import java.io.IOException;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.Context;
import javax.xml.stream.XMLStreamException;

/**
Expand All @@ -114,6 +122,7 @@
public class Dataverses extends AbstractApiBean {

private static final Logger logger = Logger.getLogger(Dataverses.class.getCanonicalName());
private static final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss");

@EJB
ExplicitGroupServiceBean explicitGroupSvc;
Expand All @@ -123,6 +132,12 @@ public class Dataverses extends AbstractApiBean {

@EJB
SettingsServiceBean settingsService;

@EJB
GuestbookResponseServiceBean guestbookResponseService;

@EJB
GuestbookServiceBean guestbookService;

@POST
public Response addRoot(String body) {
Expand Down Expand Up @@ -832,7 +847,49 @@ public Response getGroupByOwnerAndAliasInOwner(@PathParam("identifier") String d
req,
grpAliasInOwner))));
}

@GET
@Path("{identifier}/guestbookResponses/")
@Produces({"application/download"})
public Response getGuestbookResponsesByDataverse(@PathParam("identifier") String dvIdtf,
@QueryParam("guestbookId") Long gbId, @Context HttpServletResponse response) {

try {
Dataverse dv = findDataverseOrDie(dvIdtf);
User u = findUserOrDie();
DataverseRequest req = createDataverseRequest(u);
if (permissionSvc.request(req)
.on(dv)
.has(Permission.EditDataverse)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing this permission check here in the API makes me wondering if we should create a command for both the API and UI to use.

} else {
return error(Status.FORBIDDEN, "Not authorized");
}

String fileTimestamp = dateFormatter.format(new Date());
String filename = dv.getAlias() + "_GBResponses_" + fileTimestamp + ".csv";

response.setHeader("Content-Disposition", "attachment; filename="
+ filename);
ServletOutputStream outputStream = response.getOutputStream();

Map<Integer, Object> customQandAs = guestbookResponseService.mapCustomQuestionAnswersAsStrings(dv.getId(), gbId);

List<Object[]> guestbookResults = guestbookResponseService.getGuestbookResults(dv.getId(), gbId);
outputStream.write("Guestbook, Dataset, Dataset PID, Date, Type, File Name, File Id, File PID, User Name, Email, Institution, Position, Custom Questions\n".getBytes());
for (Object[] result : guestbookResults) {
StringBuilder sb = guestbookResponseService.convertGuestbookResponsesToCSV(customQandAs, result);
outputStream.write(sb.toString().getBytes());
outputStream.flush();
}
return Response.ok().build();
} catch (IOException io) {
return error(Status.BAD_REQUEST, "Failed to produce response file. Exception: " + io.getMessage());
} catch (WrappedResponse wr) {
return wr.getResponse();
}

}

@PUT
@Path("{identifier}/groups/{aliasInOwner}")
public Response updateGroup(ExplicitGroupDTO groupDto,
Expand Down