Skip to content

Commit

Permalink
Merge pull request #1644 from UNC-Libraries/BXC-4358-manage-links-ser…
Browse files Browse the repository at this point in the history
…vice

Bxc-4358 manage links service
  • Loading branch information
sharonluong authored Jan 8, 2024
2 parents ae016f4 + 4f36511 commit 6f70c6f
Show file tree
Hide file tree
Showing 4 changed files with 345 additions and 18 deletions.
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();
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

0 comments on commit 6f70c6f

Please sign in to comment.