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

Bxc-4358 manage links service #1644

Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package edu.unc.lib.boxc.web.services.processing;

import edu.unc.lib.boxc.model.api.exceptions.RepositoryException;
import org.apache.commons.csv.CSVRecord;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;

import static edu.unc.lib.boxc.web.services.utils.CsvUtil.createCsvPrinter;
import static edu.unc.lib.boxc.web.services.utils.CsvUtil.createNewCsvPrinter;
import static edu.unc.lib.boxc.web.services.utils.CsvUtil.parseCsv;

/**
* Generate and invalidate access keys for single use links
* @author snluong
*/
public class SingleUseKeyService {
public static final String ID = "UUID";
public static final String ACCESS_KEY = "Access Key";
public static final String TIMESTAMP = "Expiration Timestamp";
public static final String[] CSV_HEADERS = new String[] {ID, ACCESS_KEY, TIMESTAMP};
public static final long DAY_MILLISECONDS = 86400000;
private Path csvPath;
private ReentrantLock lock = new ReentrantLock();

/**
* Generates an access key for a particular ID, adds it to the CSV, and returns the key
* @param id UUID of the record
* @return generated access key
*/
public String generate(String id) {
var key = getKey();
lock.lock();
sharonluong marked this conversation as resolved.
Show resolved Hide resolved
var expirationInMilliseconds = System.currentTimeMillis() + DAY_MILLISECONDS;
try (var csvPrinter = createCsvPrinter(CSV_HEADERS, csvPath)) {
csvPrinter.printRecord(id, key, expirationInMilliseconds);
} catch (Exception e) {
throw new RepositoryException("Failed to write new key to Single Use Key CSV", e);
} finally {
lock.unlock();
}
return key;
}

/**
* Determines if a key is valid by seeing if it is in the CSV and if the expiration timestamp has not passed
* @param key access key for single use link
* @return true if key is in the CSV, otherwise false
*/
public boolean keyIsValid(String key) {
try {
var csvRecords = parseCsv(CSV_HEADERS, csvPath);
var currentMilliseconds = System.currentTimeMillis();
for (CSVRecord record : csvRecords) {
if (key.equals(record.get(ACCESS_KEY))) {
var expirationTimestamp = Long.parseLong(record.get(TIMESTAMP));
return currentMilliseconds <= expirationTimestamp;
}
}
} catch (IOException e) {
throw new RepositoryException("Failed to determine if key is valid in Single Use Key CSV", e);
}
return false;
}

/**
* Invalidates a key by removing its entry from the CSV
* @param key access key of the box-c record
*/
public void invalidate(String key) {
lock.lock();
try {
var csvRecords = parseCsv(CSV_HEADERS, csvPath);
var updatedRecords = new ArrayList<>();
var keyExists = false;
for (CSVRecord record : csvRecords) {
if (key.equals(record.get(ACCESS_KEY))) {
keyExists = true;
} else {
// add the rest of the keys to list
updatedRecords.add(record);
}
}

if (keyExists) {
try (var csvPrinter = createNewCsvPrinter(CSV_HEADERS, csvPath)) {
csvPrinter.flush();
csvPrinter.printRecords(updatedRecords);
}
}
} catch (IOException e) {
throw new RepositoryException("Failed to invalidate key in Single Use Key CSV", e);
} finally {
lock.unlock();
}
}

public static String getKey() {
return UUID.randomUUID().toString().replace("-", "") + Long.toHexString(System.nanoTime());
}

public void setCsvPath(Path csvPath) {
this.csvPath = csvPath;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package edu.unc.lib.boxc.web.services.utils;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.csv.CSVRecord;

import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;

/**
* Util for CSV-related operations
* @author snluong
*/
public class CsvUtil {
public static List<CSVRecord> parseCsv(String[] headers, Path csvPath) throws IOException {
Reader reader = Files.newBufferedReader(csvPath);
return new CSVParser(reader, CSVFormat.DEFAULT
.withFirstRecordAsHeader()
.withHeader(headers)
.withTrim())
.getRecords();
}

/**
* Creates a CSV printer and determines whether new rows should be appended to existing CSV
* @param headers header values of the CSV
* @param csvPath path of the CSV
* @return
* @throws IOException
*/
public static CSVPrinter createCsvPrinter(String[] headers, Path csvPath) throws IOException {
if (Files.exists(csvPath)) {
var writer = Files.newBufferedWriter(csvPath, StandardOpenOption.APPEND);
return new CSVPrinter(writer, CSVFormat.DEFAULT.withSkipHeaderRecord());
} else {
return createNewCsvPrinter(headers, csvPath);
}
}

/**
* Make a new CSV printer that does not append new rows
* @param headers header values of the CSV
* @param csvPath path of the CSV
* @return
* @throws IOException
*/
public static CSVPrinter createNewCsvPrinter(String[] headers, Path csvPath) throws IOException {
var writer = Files.newBufferedWriter(csvPath);
return new CSVPrinter(writer, CSVFormat.DEFAULT
.withHeader(headers));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
import edu.unc.lib.boxc.search.solr.models.DatastreamImpl;
import edu.unc.lib.boxc.search.solr.responses.SearchResultResponse;
import edu.unc.lib.boxc.search.solr.services.SolrSearchService;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
Expand All @@ -26,14 +24,12 @@
import org.mockito.Mock;

import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import static edu.unc.lib.boxc.search.api.FacetConstants.MARKED_FOR_DELETION;
import static edu.unc.lib.boxc.web.services.utils.CsvUtil.parseCsv;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Matchers.any;
Expand Down Expand Up @@ -88,7 +84,7 @@ public void exportUnorderedObjectTest() throws Exception {
mockChildrenResults(rec1, rec2);

var resultPath = csvService.export(asPidList(PARENT1_UUID), agent);
var csvRecords = parseCsv(resultPath);
var csvRecords = parseCsv(MemberOrderCsvConstants.CSV_HEADERS, resultPath);
assertNumberOfEntries(2, csvRecords);
assertContainsEntry(csvRecords, CHILD1_UUID, PARENT1_UUID, "File One",
"file1.txt", "text/plain", false, null);
Expand All @@ -107,7 +103,7 @@ public void exportPartiallyOrderedObjectTest() throws Exception {
mockChildrenResults(rec1, rec2);

var resultPath = csvService.export(asPidList(PARENT1_UUID), agent);
var csvRecords = parseCsv(resultPath);
var csvRecords = parseCsv(MemberOrderCsvConstants.CSV_HEADERS, resultPath);
assertContainsEntry(csvRecords, CHILD1_UUID, PARENT1_UUID, "File One",
"file1.txt", "text/plain", false, 0);
assertContainsEntry(csvRecords, CHILD2_UUID, PARENT1_UUID, "File Two",
Expand All @@ -127,7 +123,7 @@ public void exportOrderedObjectWithDeletedChildTest() throws Exception {
mockChildrenResults(rec1, rec2);

var resultPath = csvService.export(asPidList(PARENT1_UUID), agent);
var csvRecords = parseCsv(resultPath);
var csvRecords = parseCsv(MemberOrderCsvConstants.CSV_HEADERS, resultPath);
assertContainsEntry(csvRecords, CHILD1_UUID, PARENT1_UUID, "File One",
"file1.txt", "text/plain", false, 0);
assertContainsEntry(csvRecords, CHILD2_UUID, PARENT1_UUID, "File Two",
Expand All @@ -151,7 +147,7 @@ public void exportMultipleOrderedObjectsTest() throws Exception {
.thenReturn(makeResultResponse(rec3));

var resultPath = csvService.export(asPidList(PARENT1_UUID, PARENT2_UUID), agent);
var csvRecords = parseCsv(resultPath);
var csvRecords = parseCsv(MemberOrderCsvConstants.CSV_HEADERS, resultPath);
assertContainsEntry(csvRecords, CHILD1_UUID, PARENT1_UUID, "File One",
"file1.txt", "text/plain", false, 0);
assertContainsEntry(csvRecords, CHILD2_UUID, PARENT1_UUID, "File Two",
Expand Down Expand Up @@ -253,15 +249,6 @@ private List<PID> asPidList(String... ids) {
return Arrays.stream(ids).map(PIDs::get).collect(Collectors.toList());
}

private List<CSVRecord> parseCsv(Path csvPath) throws IOException {
Reader reader = Files.newBufferedReader(csvPath);
return new CSVParser(reader, CSVFormat.DEFAULT
.withFirstRecordAsHeader()
.withHeader(MemberOrderCsvConstants.CSV_HEADERS)
.withTrim())
.getRecords();
}

private ContentObjectRecord makeWorkRecord(String uuid, String title) {
return makeRecord(uuid, COLLECTION_UUID, ResourceType.Work, title, null, null, null);
}
Expand Down
Loading
Loading