Complete Code#
Expense
Class#
package com.example.gettingstarted;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.inrupt.client.solid.Metadata;
import com.inrupt.client.solid.SolidRDFSource;
import com.inrupt.rdf.wrapping.commons.RDFFactory;
import com.inrupt.rdf.wrapping.commons.TermMappings;
import com.inrupt.rdf.wrapping.commons.ValueMappings;
import com.inrupt.rdf.wrapping.commons.WrapperIRI;
import org.apache.commons.rdf.api.Dataset;
import org.apache.commons.rdf.api.Graph;
import org.apache.commons.rdf.api.IRI;
import org.apache.commons.rdf.api.RDFTerm;
import java.math.BigDecimal;
import java.net.URI;
import java.time.Instant;
import java.util.Date;
import java.util.Objects;
import java.util.*;
/**
* Part 1
* Note: extends SolidRDFSource
* To model the Expense class as an RDF resource, the Expense class extends SolidRDFSource.
* <p>
* The @JsonIgnoreProperties annotation is added to ignore the RDF-related fields
* ("metadata", "graph", "graphNames", "entity", "contentType") when serializing Expense data as JSON.
*/
@JsonIgnoreProperties(value = { "metadata", "graph", "graphNames", "entity", "contentType"})
public class Expense extends SolidRDFSource {
/**
* Note 2a: Predicate Definitions
* The following constants define the Predicates used in our triple statements.
*/
static IRI RDF_TYPE = rdf.createIRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
static IRI SCHEMA_DATE = rdf.createIRI("https://schema.org/purchaseDate");
static IRI SCHEMA_PROVIDER = rdf.createIRI("https://schema.org/provider");
static IRI SCHEMA_DESCRIPTION = rdf.createIRI("https://schema.org/description");
static IRI SCHEMA_TOTAL_PRICE = rdf.createIRI("https://schema.org/totalPrice");
static IRI SCHEMA_CURRENCY = rdf.createIRI("https://schema.org/priceCurrency");
static IRI SCHEMA_CATEGORY = rdf.createIRI("https://schema.org/category");
// Added predicate for receipts
static IRI SCHEMA_IMAGE = rdf.createIRI("https://schema.org/image");
/**
* Note 2b: Value Definition
* The following constant define the value for the predicate RDF_TYPE.
*/
static URI MY_RDF_TYPE_VALUE = URI.create("https://schema.org/Invoice");
/**
* Note 3: Node class
* The Node class is an inner class (defined below) that handles the mapping between expense data and RDF triples.
* The subject contains the expense data.
*/
private final Node subject;
/**
* Note 4: Constructors
* Expense constructors to handle SolidResource fields:
* - identifier: The destination URI of the resource; e.g., https://myPod.example.com/myPod/expense1
* - dataset: The org.apache.commons.rdf.api.Dataset that corresponding to the resource. Can be null when instantiating the class.
* - metadata: The com.inrupt.client.solid.Metadata that contains information from response headers. Can be null when instantiating the class.
* <p>
* In addition, the subject field is initialized.
*/
public Expense(final URI identifier, final Dataset dataset, final Metadata metadata) {
super(identifier, dataset, metadata);
this.subject = new Node(rdf.createIRI(identifier.toString()), getGraph());
}
public Expense(final URI identifier) {
this(identifier, null, null);
}
// Constructor updated to handle receipts
@JsonCreator
public Expense(@JsonProperty("identifier") final URI identifier,
@JsonProperty("merchantProvider") String merchantProvider,
@JsonProperty("expenseDate") Date expenseDate,
@JsonProperty("description") String description,
@JsonProperty("amount") BigDecimal amount,
@JsonProperty("currency") String currency,
@JsonProperty("category") String category,
@JsonProperty("receipts") String[] receipts) {
this(identifier);
this.setRdfType(MY_RDF_TYPE_VALUE);
this.setMerchantProvider(merchantProvider);
this.setExpenseDate(expenseDate);
this.setDescription(description);
this.setAmount(amount);
this.setCurrency(currency);
this.setCategory(category);
this.setReceipts(receipts);
}
/**
* Note 5: Various getters/setters.
* The getters and setters reference the subject's methods.
*/
public URI getRDFType() {
return subject.getRDFType();
}
public void setRdfType(URI rdfType) {
subject.setRDFType(rdfType);
}
public String getMerchantProvider() {
return subject.getMerchantProvider();
}
public void setMerchantProvider(String merchantProvider) {
subject.setMerchantProvider(merchantProvider);
}
public Date getExpenseDate() {
return subject.getExpenseDate();
}
public void setExpenseDate(Date expenseDate) {
subject.setExpenseDate(expenseDate);
}
public String getDescription() {
return subject.getDescription();
}
public void setDescription(String description) {
subject.setDescription(description);
}
public BigDecimal getAmount() {
return subject.getAmount();
}
public void setAmount(BigDecimal amount) {
subject.setAmount(amount);
}
public String getCurrency() {
return subject.getCurrency();
}
public void setCurrency(String currency) {
subject.setCurrency(currency);
}
public String getCategory() {
return subject.getCategory();
}
public void setCategory(String category) {
subject.setCategory(category);
}
// Expense class: getter and setters for receipts
public Set<String> getReceipts() {
return subject.getReceipts();
}
// Note:: The setters first uses the getter, which returns a Set, and adds the receipt to the set.
public void addReceipt(String receipt) {
subject.getReceipts().add(receipt);
}
public void setReceipts(String[] receipts) {
subject.getReceipts().addAll(List.of(receipts));
}
/**
* Note 6: Inner class ``Node`` that extends WrapperIRI
* Node class handles the mapping of the expense data (date, provider,
* description, category, priceCurrency, total) to RDF triples
* <subject> <predicate> <object>.
* <p>
* Nomenclature Background: A set of RDF triples is called a Graph.
*/
class Node extends WrapperIRI {
Node(final RDFTerm original, final Graph graph) {
super(original, graph);
}
URI getRDFType() {
return anyOrNull(RDF_TYPE, ValueMappings::iriAsUri);
}
/**
* Note 7: In its getters, the ``Node`` class calls WrapperBlankNodeOrIRI
* method ``anyOrNull`` to return either 0 or 1 value mapped to the predicate.
* You can use ValueMappings method to convert the value to a specified type.
* <p>
* In its setters, the ``Node`` class calls WrapperBlankNodeOrIRI
* method ``overwriteNullable`` to return either 0 or 1 value mapped to the predicate.
* You can use TermMappings method to store the value with the specified type information.
*/
void setRDFType(URI type) {
overwriteNullable(RDF_TYPE, type, TermMappings::asIri);
}
String getMerchantProvider() {
return anyOrNull(SCHEMA_PROVIDER, ValueMappings::literalAsString);
}
void setMerchantProvider(String provider) {
overwriteNullable(SCHEMA_PROVIDER, provider, TermMappings::asStringLiteral);
}
public Date getExpenseDate() {
Instant expenseInstant = anyOrNull(SCHEMA_DATE, ValueMappings::literalAsInstant);
if (expenseInstant != null) return Date.from(expenseInstant);
else return null;
}
public void setExpenseDate(Date expenseDate) {
overwriteNullable(SCHEMA_DATE, expenseDate.toInstant(), TermMappings::asTypedLiteral);
}
String getDescription() {
return anyOrNull(SCHEMA_DESCRIPTION, ValueMappings::literalAsString);
}
void setDescription(String description) {
overwriteNullable(SCHEMA_DESCRIPTION, description, TermMappings::asStringLiteral);
}
public BigDecimal getAmount() {
String priceString = anyOrNull(SCHEMA_TOTAL_PRICE, ValueMappings::literalAsString);
if (priceString != null) return new BigDecimal(priceString);
else return null;
}
/**
* Note 8: You can write your own TermMapping helper.
*/
public void setAmount(BigDecimal totalPrice) {
overwriteNullable(SCHEMA_TOTAL_PRICE, totalPrice, (final BigDecimal value, final Graph graph) -> {
Objects.requireNonNull(value, "Value must not be null");
Objects.requireNonNull(graph, "Graph must not be null");
return RDFFactory.getInstance().
createLiteral(
value.toString(),
RDFFactory.getInstance().createIRI("http://www.w3.org/2001/XMLSchema#decimal")
);
});
}
public String getCurrency() {
return anyOrNull(SCHEMA_CURRENCY, ValueMappings::literalAsString);
}
public void setCurrency(String currency) {
overwriteNullable(SCHEMA_CURRENCY, currency, TermMappings::asStringLiteral);
}
public String getCategory() {
return anyOrNull(SCHEMA_CATEGORY, ValueMappings::literalAsString);
}
public void setCategory(String category) {
overwriteNullable(SCHEMA_CATEGORY, category, TermMappings::asStringLiteral);
}
// Node class: Added getter for receipts
public Set<String> getReceipts() {
return objects(SCHEMA_IMAGE, TermMappings::asIri, ValueMappings::iriAsString);
}
// No setter added
}
}
package com.example.gettingstarted
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import com.inrupt.client.solid.Metadata
import com.inrupt.client.solid.SolidRDFSource
import com.inrupt.rdf.wrapping.commons.RDFFactory
import com.inrupt.rdf.wrapping.commons.TermMappings
import com.inrupt.rdf.wrapping.commons.ValueMappings
import com.inrupt.rdf.wrapping.commons.WrapperIRI
import org.apache.commons.rdf.api.Dataset
import org.apache.commons.rdf.api.Graph
import org.apache.commons.rdf.api.IRI
import org.apache.commons.rdf.api.RDFTerm
import java.math.BigDecimal
import java.net.URI
import java.time.Instant
import java.util.*
/**
* Part 1
* Note: extends SolidRDFSource
* To model the Expense class as an RDF resource, the Expense class extends SolidRDFSource.
*
*
* The @JsonIgnoreProperties annotation is added to ignore the RDF-related fields
* ("metadata", "graph", "graphNames", "entity", "contentType") when serializing Expense data as JSON.
*/
@JsonIgnoreProperties(value = ["metadata", "graph", "graphNames", "entity", "contentType"])
class Expense(
identifier: URI,
dataset: Dataset = rdf.createDataset(),
metadata: Metadata = Metadata.newBuilder().build()
) : SolidRDFSource(identifier, dataset, metadata) {
companion object {
/**
* Note 2a: Predicate Definitions
* The following constants define the Predicates used in our triple statements.
*/
var RDF_TYPE: IRI = rdf.createIRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")
var SCHEMA_DATE: IRI = rdf.createIRI("https://schema.org/purchaseDate")
var SCHEMA_PROVIDER: IRI = rdf.createIRI("https://schema.org/provider")
var SCHEMA_DESCRIPTION: IRI = rdf.createIRI("https://schema.org/description")
var SCHEMA_TOTAL_PRICE: IRI = rdf.createIRI("https://schema.org/totalPrice")
var SCHEMA_CURRENCY: IRI = rdf.createIRI("https://schema.org/priceCurrency")
var SCHEMA_CATEGORY: IRI = rdf.createIRI("https://schema.org/category")
// Added predicate for receipts
var SCHEMA_IMAGE: IRI = rdf.createIRI("https://schema.org/image")
/**
* Note 2b: Value Definition
* The following constant defines the value for the predicate RDF_TYPE.
*/
var MY_RDF_TYPE_VALUE: URI = URI.create("https://schema.org/Invoice")
}
/**
* Note 3: Node class
* The Node class is an inner class (defined below) that handles the mapping between expense data and RDF triples.
* The subject contains the expense data.
*/
private val subject: Node = Node(rdf.createIRI(identifier.toString()), dataset.graph)
// Constructor updated to handle receipts
@JsonCreator
constructor(
@JsonProperty("identifier") identifier: URI,
@JsonProperty("merchantProvider") merchantProvider: String?,
@JsonProperty("expenseDate") expenseDate: Date,
@JsonProperty("description") description: String?,
@JsonProperty("amount") amount: BigDecimal?,
@JsonProperty("currency") currency: String?,
@JsonProperty("category") category: String?,
@JsonProperty("receipts") receipts: Set<String>?
) : this(identifier, rdf.createDataset(), Metadata.newBuilder().build()) {
this.rdfType = MY_RDF_TYPE_VALUE
this.merchantProvider = merchantProvider
this.expenseDate = expenseDate
this.description = description
this.amount = amount
this.currency = currency
this.category = category
this.receipts = receipts
}
// Constructor with just the Identifier
constructor(
identifier: URI
) : this(identifier, rdf.createDataset(), Metadata.newBuilder().build())
/**
* Note 5: Various getters/setters.
* The getters and setters reference the subject's methods.
*/
var rdfType: URI?
get() = subject.rdfType
set(rdfType) {
subject.rdfType = rdfType
}
var merchantProvider: String?
get() = subject.merchantProvider
set(merchantProvider) {
subject.merchantProvider = merchantProvider
}
var expenseDate: Date?
get() = subject.expenseDate
set(expenseDate) {
subject.expenseDate = expenseDate
}
var description: String?
get() = subject.description
set(description) {
subject.description = description
}
var amount: BigDecimal?
get() = subject.amount
set(amount) {
subject.amount = amount
}
var currency: String?
get() = subject.currency
set(currency) {
subject.currency = currency
}
var category: String?
get() = subject.category
set(category) {
subject.category = category
}
// Expense class: getter and setters for receipts
// Note:: The setters first uses the getter, which returns a Set, and adds the receipt to the set.
var receipts: Set<String>?
get() = subject.receipts
set(receipts) {
if (receipts != null) {
subject.receipts.addAll(receipts)
}
}
fun addReceipt(receipt: String) {
subject.receipts.add(receipt)
}
/**
* Note 6: Inner class ``Node`` that extends WrapperIRI
* Node class handles the mapping of the expense data (date, provider,
* description, category, priceCurrency, total) to RDF triples
* <subject> <predicate> <object>.
*
* Nomenclature Background: A set of RDF triples is called a Graph.
*/
internal class Node(original: RDFTerm, graph: Graph) :
WrapperIRI(original, graph) {
/**
* Note 7: In its getters, the ``Node`` class calls WrapperBlankNodeOrIRI
* method ``anyOrNull`` to return either 0 or 1 value mapped to the predicate.
* You can use ValueMappings method to convert the value to a specified type.
*
* In its setters, the ``Node`` class calls WrapperBlankNodeOrIRI
* method ``overwriteNullable`` to return either 0 or 1 value mapped to the predicate.
* You can use the TermMappings method to store the value with the specified type information.
*/
var rdfType: URI?
get() = anyOrNull(RDF_TYPE) { term: RDFTerm, graph: Graph ->
ValueMappings.iriAsUri(term, graph)
}
set(type) {
overwriteNullable(RDF_TYPE, type) { value: URI?, graph: Graph ->
TermMappings.asIri(value, graph)
}
}
var merchantProvider: String?
get() = anyOrNull(SCHEMA_PROVIDER) { term: RDFTerm, graph: Graph ->
ValueMappings.literalAsString(term, graph)
}
set(provider) {
overwriteNullable(SCHEMA_PROVIDER, provider) { value: String?, graph: Graph ->
TermMappings.asStringLiteral(value, graph)
}
}
var expenseDate: Date?
get() {
val expenseInstant: Instant? = anyOrNull(SCHEMA_DATE) { term: RDFTerm, graph: Graph ->
ValueMappings.literalAsInstant(term, graph)
}
return if (expenseInstant != null) Date.from(expenseInstant) else null
}
set(expenseDate) {
val expenseInstant: Instant? = if (expenseDate != null) expenseDate.toInstant() else null
overwriteNullable(SCHEMA_DATE, expenseInstant) { value: Instant?, graph: Graph ->
TermMappings.asTypedLiteral(value, graph)
}
}
var description: String?
get() = anyOrNull(SCHEMA_DESCRIPTION) { term: RDFTerm?, graph: Graph ->
ValueMappings.literalAsString(term, graph)
}
set(description) {
overwriteNullable(SCHEMA_DESCRIPTION, description) { value: String?, graph: Graph ->
TermMappings.asStringLiteral(value, graph)
}
}
/**
* Note 8: You can write your own TermMapping helper.
*/
var amount: BigDecimal?
get() {
val priceString: String? = anyOrNull(SCHEMA_TOTAL_PRICE) { term: RDFTerm?, graph: Graph ->
ValueMappings.literalAsString(term, graph)
}
return if (priceString != null) BigDecimal(priceString) else null
}
set(totalPrice) {
overwriteNullable(SCHEMA_TOTAL_PRICE, totalPrice) { value, _ ->
RDFFactory.getInstance().createLiteral(
value.toString(),
RDFFactory.getInstance().createIRI("http://www.w3.org/2001/XMLSchema#decimal")
)
}
}
var currency: String?
get() = anyOrNull(SCHEMA_CURRENCY) { term: RDFTerm?, graph: Graph ->
ValueMappings.literalAsString(term, graph)
}
set(currency) {
overwriteNullable(SCHEMA_CURRENCY, currency) { value: String?, graph: Graph ->
TermMappings.asStringLiteral(value, graph)
}
}
var category: String?
get() = anyOrNull(SCHEMA_CATEGORY) { term: RDFTerm?, graph: Graph ->
ValueMappings.literalAsString(term, graph)
}
set(category) {
overwriteNullable(SCHEMA_CATEGORY, category) { value: String?, graph: Graph ->
TermMappings.asStringLiteral(value, graph)
}
}
// Node class: Added getter for receipts
val receipts: MutableSet<String>
get() = objects(SCHEMA_IMAGE, TermMappings::asIri, ValueMappings::iriAsString)
// No setter added
}
}
ExpenseController
Class#
package com.example.gettingstarted;
import com.inrupt.client.Request;
import com.inrupt.client.Response;
import com.inrupt.client.auth.Session;
import com.inrupt.client.openid.OpenIdSession;
import com.inrupt.client.solid.SolidSyncClient;
import com.inrupt.client.webid.WebIdProfile;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.io.PrintWriter;
import java.net.URI;
import java.util.Set;
import org.springframework.web.multipart.MultipartFile;
@RequestMapping("/api")
@RestController
public class ExpenseController {
/**
* Note 1: Authenticated Session
* Using the client credentials, create an authenticated session.
*/
final Session session = OpenIdSession.ofClientCredentials(
URI.create(System.getenv("MY_SOLID_IDP")),
System.getenv("MY_SOLID_CLIENT_ID"),
System.getenv("MY_SOLID_CLIENT_SECRET"),
System.getenv("MY_AUTH_FLOW"));
/**
* Note 2: SolidSyncClient
* Instantiates a synchronous client for the authenticated session.
* The client has methods to perform CRUD operations.
*/
final SolidSyncClient client = SolidSyncClient.getClient().session(session);
private final PrintWriter printWriter = new PrintWriter(System.out, true);
/**
* Note 3: SolidSyncClient.read()
* Using the SolidSyncClient client.read() method, reads the user's WebID Profile document and returns the Pod URI(s).
*/
@GetMapping("/pods")
public Set<URI> getPods(@RequestParam(value = "webid", defaultValue = "") String webID) {
printWriter.println("ExpenseController:: getPods");
try (final var profile = client.read(URI.create(webID), WebIdProfile.class)) {
return profile.getStorage();
}
}
/**
* Note 4: SolidSyncClient.create()
* Using the SolidSyncClient client.create() method,
* - Saves the Expense as an RDF resource to the location specified in the Expense.identifier field.
*/
@PostMapping(path = "/expenses/create")
public String createExpense(@RequestBody Expense newExpense) {
printWriter.println("ExpenseController:: createExpense");
client.create(newExpense);
return getResourceAsTurtle(String.valueOf(newExpense.getIdentifier()));
}
/**
* Note 5: SolidSyncClient.read()
* Using the SolidSyncClient client.read() method,
* - Reads the RDF resource into the Expense class.
*/
@GetMapping("/expenses/get")
public Expense getExpense(@RequestParam(value = "resourceURL", defaultValue = "") String resourceURL) {
printWriter.println("ExpenseController:: getExpense");
try (var resource = client.read(URI.create(resourceURL), Expense.class)) {
return resource;
} catch (Exception e) {
e.printStackTrace();
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
}
/**
* Note 6: SolidSyncClient.update()
* Using the SolidSyncClient client.update() method,
* - Updates the Expense resource.
*/
@PutMapping("/expenses/update")
public String updateExpense(@RequestBody Expense expense) {
printWriter.println("ExpenseController:: updateExpense");
client.update(expense);
return getResourceAsTurtle(String.valueOf(expense.getIdentifier()));
}
/**
* Note 7: SolidSyncClient.delete()
* Using the SolidSyncClient client.delete() method,
* - Deletes the resource located at the resourceURL.
*/
@DeleteMapping("/expenses/delete")
public void deleteExpense(@RequestParam(value = "resourceURL") String resourceURL) {
printWriter.println("ExpenseController:: deleteExpense");
try {
client.delete(URI.create(resourceURL));
// Alternatively, you can specify an Expense object to the delete method.
// The delete method deletes the Expense recorde located in the Expense.identifier field.
// For example: client.delete(new Expense(URI.create(resourceURL)));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Note 8: Build and issue custom GET Request
* - Use Inrupt's Request builder to create a GET Request to get the resource as MIME-type "text/turtle".
* - Use SolidSyncClient.send() to send the Request and return the response body.
*/
@GetMapping("/resource/get")
public String getResourceAsTurtle(@RequestParam(value = "resourceURL", defaultValue = "") String resourceURL) {
Request request = Request.newBuilder()
.uri(URI.create(resourceURL))
.header("Accept", "text/turtle")
.GET()
.build();
Response<String> response = client.send(
request,
Response.BodyHandlers.ofString());
return response.body();
}
/**
* Note 9: Stores a non-RDF resource to a Pod
*
* - Build a PUT request using Request.newBuilder()...build();
* - Send the request using SolidSyncClient's low-level client.send() method;
*/
@PutMapping("/resource/nonRDF/add")
public boolean addNonRDFFile(@RequestParam(value = "destinationURL") String destinationURL,
@RequestParam(value = "file") MultipartFile file) {
printWriter.println("In addNonRDFFile:: Save Non-RDF File to Pod.");
try (final var fileStream = file.getInputStream()) {
Request request = Request.newBuilder()
.uri(URI.create(destinationURL))
.header("Content-Type", file.getContentType())
.PUT(Request.BodyPublishers.ofInputStream(fileStream))
.build();
Response<Void> response = client.send(
request,
Response.BodyHandlers.discarding());
if (response.statusCode() == 201 || response.statusCode() == 200 || response.statusCode() == 204)
return true;
} catch (java.io.IOException e1) {
e1.printStackTrace();
}
return false;
}
/**
* Note 10: Stores a non-RDF resource (image of the receipt) to a Pod and Attach to an Expense
* Using methods defined as part of getting started, addReceiptToExpense:
* - Calls addNonRDFFile() to store the receipt to a Pod
* - Calls getExpense() to fetch the associated Expense RDF resource.
* - Calls the Expense's setter `addReceipt` to add the link to the saved receipt.
* - Calls updateExpense() to save the updated Expense.
*/
@PutMapping("/expenses/receipts/add")
public String addReceiptToExpense(@RequestParam(value = "destinationURL") String destinationURL,
@RequestParam(value = "file") MultipartFile file,
@RequestParam(value = "expenseURL") String expenseURL) {
printWriter.println("In addReceiptToExpense: Save Receipt File to Pod and Update Associated Expense.");
boolean success = addNonRDFFile(destinationURL, file);
if (success) {
Expense expense = getExpense(expenseURL);
expense.addReceipt(destinationURL);
return updateExpense(expense);
} else {
printWriter.println("Error adding receipt");
return null;
}
}
}
package com.example.gettingstarted
import com.inrupt.client.Request
import com.inrupt.client.Response
import com.inrupt.client.openid.OpenIdSession
import com.inrupt.client.solid.SolidSyncClient
import com.inrupt.client.webid.WebIdProfile
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.server.ResponseStatusException
import java.io.PrintWriter
import java.net.URI
@RequestMapping("/api")
@RestController
class ExpenseController {
/**
* Note 1: Authenticated Session
* Using the client credentials, create an authenticated session.
*/
val session = OpenIdSession.ofClientCredentials(
URI.create(System.getenv("MY_SOLID_IDP")),
System.getenv("MY_SOLID_CLIENT_ID"),
System.getenv("MY_SOLID_CLIENT_SECRET"),
System.getenv("MY_AUTH_FLOW")
)
/**
* Note 2: SolidSyncClient
* Instantiates a synchronous client for the authenticated session.
* The client has methods to perform CRUD operations.
*/
val client = SolidSyncClient.getClient().session(session)
private val printWriter = PrintWriter(System.out, true)
/**
* Note 3: SolidSyncClient.read()
* Using the SolidSyncClient client.read() method, reads the user's WebID Profile document and returns the Pod URI(s).
*/
@GetMapping("/pods")
fun getPods(@RequestParam(value = "webid", defaultValue = "") webID: String): Set<URI> {
printWriter.println("ExpenseController:: getPods")
client.read(URI.create(webID), WebIdProfile::class.java).use { profile -> return profile.storage }
}
/**
* Note 4: SolidSyncClient.create()
* Using the SolidSyncClient client.create() method,
* - Saves the Expense as an RDF resource to the location specified in the Expense.identifier field.
*/
@PostMapping("/expenses/create")
fun createExpense(@RequestBody newExpense: Expense): String {
printWriter.println("ExpenseController:: createExpense")
client.create(newExpense)
return getResourceAsTurtle(newExpense.identifier.toString())
}
/**
* Note 5: SolidSyncClient.read()
* Using the SolidSyncClient client.read() method,
* - Reads the RDF resource into the Expense class.
*/
@GetMapping("/expenses/get")
fun getExpense(
@RequestParam(
value = "resourceURL", defaultValue = ""
) resourceURL: String
): Expense {
printWriter.println("ExpenseController:: getExpense")
try {
client.read(
URI.create(resourceURL), Expense::class.java
).use { resource -> return resource }
} catch (e: Exception) {
e.printStackTrace()
throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
}
/**
* Note 6: SolidSyncClient.update()
* Using the SolidSyncClient client.update() method,
* - Updates the Expense resource.
*/
@PutMapping("/expenses/update")
fun updateExpense(@RequestBody expense: Expense): String {
printWriter.println("ExpenseController:: updateExpense")
client.update(expense)
return getResourceAsTurtle(expense.identifier.toString())
}
/**
* Note 7: SolidSyncClient.delete()
* Using the SolidSyncClient client.delete() method,
* - Deletes the resource located at the resourceURL.
*/
@DeleteMapping("/expenses/delete")
fun deleteExpense(@RequestParam(value = "resourceURL") resourceURL: String) {
printWriter.println("ExpenseController:: deleteExpense")
try {
client.delete(URI.create(resourceURL))
// Alternatively, you can specify an Expense object to the delete method.
// The delete method deletes the Expense recorde located in the Expense.identifier field.
// For example: client.delete(new Expense(URI.create(resourceURL)));
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* Note 8: Build and issue custom GET Request
* - Use Inrupt's Request builder to create a GET Request to get the resource as MIME-type "text/turtle".
* - Use SolidSyncClient.send() to send the Request and return the response body.
*/
@GetMapping("/resource/get")
fun getResourceAsTurtle(
@RequestParam(
value = "resourceURL", defaultValue = ""
) resourceURL: String
): String {
val request = Request.newBuilder().uri(URI.create(resourceURL)).header("Accept", "text/turtle").GET().build()
val response = client.send(
request, Response.BodyHandlers.ofString()
)
return response.body()
}
/**
* Note 9: Stores a non-RDF resource to a Pod
*
* - Build a PUT request using Request.newBuilder()...build();
* - Send the request using SolidSyncClient's low-level client.send() method;
*/
@PutMapping("/resource/nonRDF/add")
fun addNonRDFFile(
@RequestParam(value = "destinationURL") destinationURL: String,
@RequestParam(value = "file") file: MultipartFile
): Boolean {
printWriter.println("In addNonRDFFile: Save Non-RDF File to Pod.")
try {
file.inputStream.use { fileStream ->
var request =
Request.newBuilder().uri(URI.create(destinationURL)).header("Content-Type", file.contentType)
.PUT(Request.BodyPublishers.ofInputStream(fileStream)).build()
var response = client.send(
request, Response.BodyHandlers.discarding()
)
if (response.statusCode() == 201 || response.statusCode() == 204) {
return true
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return false
}
/**
* Note 10: Stores a non-RDF resource (image of the receipt) to a Pod and Attach to an Expense
* Using methods defined as part of getting started, addReceiptToExpense:
* - Calls addNonRDFFile() to store the receipt to a Pod
* - Calls getExpense() to fetch the associated Expense RDF resource.
* - Calls the Expense's setter `addReceipt` to add the link to the saved receipt.
* - Calls updateExpense() to save the updated Expense.
*/
@PutMapping("/expenses/receipts/add")
fun addReceiptToExpense(
@RequestParam(value = "destinationURL") destinationURL: String,
@RequestParam(value = "file") file: MultipartFile,
@RequestParam(value = "expenseURL") expenseURL: String
): String? {
printWriter.println("In AddReceiptToExpense: Save Receipt File to Pod and Update Associated Expense.")
val success = addNonRDFFile(destinationURL, file)
if (success) {
val expense = getExpense(expenseURL)
expense.addReceipt(destinationURL)
return updateExpense(expense)
} else {
printWriter.println("Error adding receipt")
return null
}
}
}