-
Notifications
You must be signed in to change notification settings - Fork 1
/
DSpaceRepository.java
535 lines (428 loc) · 22.3 KB
/
DSpaceRepository.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
package edu.tamu.app.service.repository;
import static edu.tamu.app.Initialization.ASSETS_PATH;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.commons.io.IOUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.w3c.dom.Element;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import edu.tamu.app.model.Document;
import edu.tamu.app.model.MetadataFieldGroup;
import edu.tamu.app.model.MetadataFieldValue;
import edu.tamu.app.model.ProjectRepository;
import edu.tamu.app.model.PublishedLocation;
import edu.tamu.app.model.PublishingType;
import edu.tamu.app.model.Resource;
import edu.tamu.app.model.repo.DocumentRepo;
import edu.tamu.app.model.repo.ResourceRepo;
public class DSpaceRepository extends PublishingRepository {
private static final Logger logger = LoggerFactory.getLogger(DSpaceRepository.class);
@Autowired
private ObjectMapper objectMapper;
@Autowired
private DocumentRepo documentRepo;
@Autowired
private ResourceRepo resourceRepo;
private ProjectRepository projectRepository;
private Map<Long, Optional<Cookie>> authCookies;
public DSpaceRepository(ProjectRepository projectRepository) {
this.projectRepository = projectRepository;
this.authCookies = new HashMap<Long, Optional<Cookie>>();
}
@Override
public Document push(Document document) throws IOException {
// login to get JSESSIONID
login(document);
// POST to create the item
JsonNode createItemResponseNode = null;
try {
createItemResponseNode = createItem(document);
} catch (ParserConfigurationException | TransformerException | IOException e) {
logout(document);
RuntimeException serviceEx = new RuntimeException(e.getMessage());
serviceEx.setStackTrace(e.getStackTrace());
throw serviceEx;
}
String handleString = createItemResponseNode.get("handle").asText();
String newItemIdString = createItemResponseNode.get("uuid").asText();
broadcastDocument(document.getId(), PublishingType.ITEM, "Created Item using Internal ID " + newItemIdString + ".");
// POST each of the bitstreams in this document to the newly created item
addBitstreams(document, newItemIdString);
// add new handle to document, change it's status to published, save it
String publishedUrl;
if (getRepoContextPath().length() > 0) {
publishedUrl = getRepoUrl() + "/" + getRepoContextPath() + "/" + handleString;
} else {
publishedUrl = getRepoUrl() + "/" + handleString;
}
document.addPublishedLocation(new PublishedLocation(projectRepository, publishedUrl));
broadcastDocument(document.getId(), PublishingType.MESSAGE, "Published at URL " + publishedUrl + ".");
document.setStatus("Published");
// logout to kill session
logout(document);
return documentRepo.update(document);
}
private void login(Document document) throws IOException {
try {
HttpClient httpClient = null;
CookieStore httpCookieStore = new BasicCookieStore();
HttpClientBuilder builder = HttpClientBuilder.create().setDefaultCookieStore(httpCookieStore);
httpClient = builder.build();
List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>();
nameValuePairs.add(new BasicNameValuePair("email", getEmail()));
nameValuePairs.add(new BasicNameValuePair("password", getPassword()));
HttpPost httpPost = new HttpPost(getRepoUrl() + "/rest/login");
httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs));
httpClient.execute(httpPost);
for (Cookie cookie : httpCookieStore.getCookies()) {
if (cookie.getName().equals("JSESSIONID")) {
authCookies.put(document.getId(), Optional.of(cookie));
}
}
if (!authCookies.containsKey(document.getId()) || !authCookies.get(document.getId()).isPresent()) {
throw new RuntimeException("Unable to get cookie JSESSIONID from response!");
}
logger.info("Login successful. Authorization cookie: " + getCookieAsString(authCookies.get(document.getId()).get()));
broadcastDocument(document.getId(), PublishingType.CONNECTION, "DSpace session established for repository " + projectRepository.getName() + ".");
} catch (IOException e) {
IOException ioe = new IOException("Failed to authenticate to DSpace. {" + e.getMessage() + "}");
ioe.setStackTrace(e.getStackTrace());
throw ioe;
}
}
private void logout(Document document) throws IOException {
doRESTRequest(document.getId(), new URL(getRepoUrl() + "/rest/logout"), "POST", "".getBytes(), "application/xml", "logout");
authCookies.remove(document.getId());
logger.info("Logout successful.");
broadcastDocument(document.getId(), PublishingType.CONNECTION, "DSpace session closed for repository " + projectRepository.getName() + ".");
}
private JsonNode createItem(Document document) throws ParserConfigurationException, TransformerException, IOException {
URL createItemUrl;
try {
createItemUrl = new URL(getRepoUrl() + "/rest/collections/" + getCollectionId() + "/items");
} catch (MalformedURLException e) {
logout(document);
MalformedURLException murle = new MalformedURLException("Failed to create items; the REST URL to post the item was malformed. {" + e.getMessage() + "}");
murle.setStackTrace(e.getStackTrace());
throw murle;
}
// produce the XML data from the document that we will post to the REST API
String xmlDataToPost;
try {
xmlDataToPost = generateItemPostXMLFromDocument(document);
} catch (ParserConfigurationException e) {
logout(document);
ParserConfigurationException pce = new ParserConfigurationException("Failed to create items; Could not transform document metadata into XML for the post. {" + e.getMessage() + "}");
pce.setStackTrace(e.getStackTrace());
throw pce;
} catch (TransformerFactoryConfigurationError e) {
logout(document);
TransformerFactoryConfigurationError tfce = new TransformerFactoryConfigurationError("Failed to create items; Could not transform document metadata into XML for the post. {" + e.getMessage() + "}");
tfce.setStackTrace(e.getStackTrace());
throw tfce;
} catch (TransformerException e) {
logout(document);
TransformerException te = new TransformerException("Failed to create items; Could not transform document metadata into XML for the post. {" + e.getMessage() + "}");
te.setStackTrace(e.getStackTrace());
throw te;
}
String taskDescription = "post item";
return doRESTRequest(document.getId(), createItemUrl, "POST", xmlDataToPost.getBytes(), "application/xml", taskDescription);
}
private JsonNode doRESTRequest(Long documentId, URL restUrl, String method, byte[] postData, String contentTypeString, String taskDescription) throws IOException {
logger.info("Making this REST request of DSpace: "+ taskDescription);
// set up the connection for the REST call
HttpURLConnection connection;
try {
connection = (HttpURLConnection) restUrl.openConnection();
} catch (IOException e) {
IOException ioe = new IOException("Failed to " + taskDescription + "; the REST URL to " + taskDescription + " was malformed. {" + e.getMessage() + "}");
ioe.setStackTrace(e.getStackTrace());
throw ioe;
}
try {
connection.setRequestMethod(method);
} catch (ProtocolException e) {
ProtocolException pe = new ProtocolException("Failed to " + taskDescription + "; the protocol for the request was invalid. {" + e.getMessage() + "}");
pe.setStackTrace(e.getStackTrace());
throw pe;
}
connection.setRequestProperty("Accept", "application/json");
connection.setRequestProperty("Content-Type", contentTypeString);
connection.setRequestProperty("Content-Length", String.valueOf(postData.length));
connection.setRequestProperty("Cookie", getCookieAsString(authCookies.get(documentId).get()));
logger.info("Attempting to connect to DSpace with Cookie = " + connection.getRequestProperty("Cookie"));
connection.setDoOutput(true);
// Write post data by opening an output stream on the connection and
// writing to it
OutputStream os;
try {
os = connection.getOutputStream();
} catch (IOException e) {
IOException ioe = new IOException("Failed to " + taskDescription + "; Could not open output stream to write the post data. {" + e.getMessage() + "}");
ioe.setStackTrace(e.getStackTrace());
throw ioe;
}
try {
os.write(postData);
} catch (IOException e) {
IOException ioe = new IOException("Failed to " + taskDescription + "; Could not write data to the open output stream for the post. {" + e.getMessage() + "}");
ioe.setStackTrace(e.getStackTrace());
throw ioe;
}
// Read response from item post
StringBuilder response = new StringBuilder();
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
} catch (IOException e) {
IOException ioe = new IOException("Failed to " + taskDescription + "; Could not get input stream for a response from the connection of the post request. {" + e.getMessage() + "}");
ioe.setStackTrace(e.getStackTrace());
throw ioe;
}
// read the lines of the response into the... response :)
String line;
try {
while ((line = br.readLine()) != null) {
response.append(line);
}
} catch (IOException e) {
IOException ioe = new IOException("Failed to " + taskDescription + "; Could not read a line from the response from the post. {" + e.getMessage() + "}");
ioe.setStackTrace(e.getStackTrace());
throw ioe;
}
// Close streams
try {
br.close();
} catch (IOException e) {
IOException ioe = new IOException("Failed to " + taskDescription + "; Could not close the buffered reader from which we were getting the response from the post. {" + e.getMessage() + "}");
ioe.setStackTrace(e.getStackTrace());
throw ioe;
}
try {
os.close();
} catch (IOException e) {
IOException ioe = new IOException("Failed to " + taskDescription + "; Could not close the output stream we were using to write to the post. {" + e.getMessage() + "}");
ioe.setStackTrace(e.getStackTrace());
throw ioe;
}
// parse response to get a JSON node
JsonNode responseNode = null;
if (response.length() > 0) {
try {
responseNode = objectMapper.readTree(response.toString());
} catch (IOException e) {
IOException ioe = new IOException("Failed to " + taskDescription + "; Object mapper could not read the response from the post request into JSON. {" + e.getMessage() + "}");
ioe.setStackTrace(e.getStackTrace());
throw ioe;
}
}
return responseNode;
}
private void addBitstreams(Document document, String itemId) throws IOException {
addBitstreams(document, new Bitstreams(itemId, resourceRepo.findAllByDocumentProjectNameAndDocumentNameAndMimeType(document.getProject().getName(), document.getName(), "application/pdf")));
addBitstreams(document, new Bitstreams(itemId, resourceRepo.findAllByDocumentProjectNameAndDocumentNameAndMimeType(document.getProject().getName(), document.getName(), "text/plain"), "TEXT"));
addBitstreams(document, new Bitstreams(itemId, resourceRepo.findAllByDocumentProjectNameAndDocumentNameAndMimeType(document.getProject().getName(), document.getName(), "image/jpeg", "image/jpg", "image/jp2", "image/jpx", "image/bmp", "image/gif", "image/png", "image/svg", "image/tif", "image/tiff")));
}
private void addBitstreams(Document document, Bitstreams bitstreams) throws IOException {
for (Resource resource : bitstreams.getResources()) {
// *************************************
// POST file
// *************************************
// add the bitstream
URL addBitstreamUrl;
try {
addBitstreamUrl = new URL(getRepoUrl() + "/rest/items/" + bitstreams.getItemId() + "/bitstreams?name=" + URLEncoder.encode(resource.getName(), java.nio.charset.StandardCharsets.UTF_8.toString()));
} catch (MalformedURLException e) {
MalformedURLException murle = new MalformedURLException("Failed to add pdf bitstream; the REST URL to post the bitstreams was malformed. {" + e.getMessage() + "}");
murle.setStackTrace(e.getStackTrace());
cleanUpFailedPublish(document, bitstreams.getItemId());
throw murle;
}
File file = new File(ASSETS_PATH + File.separator + resource.getPath());
FileInputStream stream = new FileInputStream(file);
byte[] bytes = IOUtils.toByteArray(stream);
stream.close();
ObjectNode bitstreamMetadataJson = null;
try {
bitstreamMetadataJson = (ObjectNode) doRESTRequest(document.getId(), addBitstreamUrl, "POST", bytes, resource.getMimeType(), "post bitstream");
} catch (Exception e) {
cleanUpFailedPublish(document, bitstreams.getItemId());
throw e;
}
// *************************************
// PUT bitstream metadata
// *************************************
// put a resource policy for member group access on the pdf bitstream
// REST endpoint is PUT /bitstreams/{bitstream uuid} - Update metadata of
// bitstream. You must put a Bitstream, does not alter the file/data
// Fix up the PDF bitstream metadata to have new policy, etc.
String uuid = bitstreamMetadataJson.get("uuid").asText();
broadcastDocument(document.getId(), PublishingType.ATTACHMENT, "Associated item " + resource.getName() + " using Internal ID " + uuid + ".");
ArrayNode policiesNode = bitstreamMetadataJson.putArray("policies");
ObjectNode policyNode = objectMapper.createObjectNode();
policyNode.put("action", "READ");
policyNode.put("groupId", getGroupId());
policyNode.put("rpType", "TYPE_CUSTOM");
policiesNode.add(policyNode);
if (bitstreams.getBundleName().isPresent()) {
bitstreamMetadataJson.put("bundleName", bitstreams.getBundleName().get());
}
URL addPolicyUrl;
try {
addPolicyUrl = new URL(getRepoUrl() + "/rest/bitstreams/" + uuid);
} catch (MalformedURLException e) {
MalformedURLException murle = new MalformedURLException("Failed to update bitstream metadata; the REST URL to PUT the policy was malformed. {" + e.getMessage() + "}");
murle.setStackTrace(e.getStackTrace());
cleanUpFailedPublish(document, bitstreams.getItemId());
throw murle;
}
try {
doRESTRequest(document.getId(), addPolicyUrl, "PUT", bitstreamMetadataJson.toString().getBytes(), "application/json", "update bitstream metadata");
} catch (Exception e) {
cleanUpFailedPublish(document, bitstreams.getItemId());
throw e;
}
broadcastDocument(document.getId(), PublishingType.MESSAGE, "Populated metadata for item " + resource.getName() + " using Internal ID " + uuid + ".");
}
}
private String generateItemPostXMLFromDocument(Document document) throws ParserConfigurationException, TransformerFactoryConfigurationError, TransformerException {
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
org.w3c.dom.Document domDoc = docBuilder.newDocument();
Element rootElement = domDoc.createElement("item");
domDoc.appendChild(rootElement);
for (MetadataFieldGroup fieldGroup : document.getFields()) {
for (MetadataFieldValue value : fieldGroup.getValues()) {
if (value.getValue() != null && !value.getValue().equals("")) {
Element metadata = domDoc.createElement("metadata");
rootElement.appendChild(metadata);
Element key = domDoc.createElement("key");
key.appendChild(domDoc.createTextNode(fieldGroup.getLabel().getName()));
metadata.appendChild(key);
Element valueElement = domDoc.createElement("value");
valueElement.appendChild(domDoc.createTextNode(value.getValue()));
metadata.appendChild(valueElement);
Element language = domDoc.createElement("language");
language.appendChild(domDoc.createTextNode("en_US"));
metadata.appendChild(language);
}
}
}
StringWriter stw = new StringWriter();
Transformer serializer = TransformerFactory.newInstance().newTransformer();
serializer.transform(new DOMSource(domDoc), new StreamResult(stw));
return stw.toString();
}
private void cleanUpFailedPublish(Document document, String uuid) throws IOException {
// delete the item in case there was an error along the way with all the requests.
// REST endpoint is DELETE /items/{item uuid} - Delete item.
logger.error("Error pushing to DSpace. Rolling back.");
broadcastDocument(document.getId(), PublishingType.ALERT, "Error pushing item " + uuid + " to DSpace. Rolling back.");
URL deleteItemUrl;
try {
deleteItemUrl = new URL(getRepoUrl() + "/rest/items/" + uuid);
} catch (MalformedURLException e) {
logout(document);
MalformedURLException murle = new MalformedURLException("Failed to delete item " + uuid + "; the REST URL for the DELETE request was malformed. {" + e.getMessage() + "}");
murle.setStackTrace(e.getStackTrace());
throw murle;
}
try {
doRESTRequest(document.getId(), deleteItemUrl, "DELETE", "".getBytes(), "application/json", "delete item");
}
catch (IOException e) {
logout(document);
throw e;
}
broadcastDocument(document.getId(), PublishingType.WARNING, "Cleaned up item " + uuid + ".");
// logout to kill session
logout(document);
}
class Bitstreams {
private String itemId;
private List<Resource> resources;
private Optional<String> bundleName;
public Bitstreams(String itemId, List<Resource> resources) {
this.itemId = itemId;
this.resources = resources;
this.bundleName = Optional.empty();
}
public Bitstreams(String itemId, List<Resource> resources, String bundleName) {
this(itemId, resources);
this.bundleName = Optional.of(bundleName);
}
public String getItemId() {
return itemId;
}
public List<Resource> getResources() {
return resources;
}
public Optional<String> getBundleName() {
return bundleName;
}
}
public String getRepoUrl() {
return getSettingValue("repoUrl");
}
public String getRepoContextPath() {
return getSettingValue("repoContextPath");
}
public String getCollectionId() {
return getSettingValue("collectionId");
}
public String getGroupId() {
return getSettingValue("groupId");
}
public String getEmail() {
return getSettingValue("email");
}
public String getPassword() {
return getSettingValue("password");
}
private String getSettingValue(String key) {
String value = projectRepository.getSettingValues(key).get(0);
return (value != null) ? value:"";
}
private String getCookieAsString(Cookie cookie) {
return String.join("=", cookie.getName(), cookie.getValue());
}
}