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.Headers;
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 non-class-member fields
* when serializing Expense data as JSON.
*/
@JsonIgnoreProperties(value = { "metadata", "headers", "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_ORG_PURCHASE_DATE = rdf.createIRI("https://schema.org/purchaseDate");
static IRI SCHEMA_ORG_PROVIDER = rdf.createIRI("https://schema.org/provider");
static IRI SCHEMA_ORG_DESCRIPTION = rdf.createIRI("https://schema.org/description");
static IRI SCHEMA_ORG_TOTAL_PRICE = rdf.createIRI("https://schema.org/totalPrice");
static IRI SCHEMA_ORG_PRICE_CURRENCY = rdf.createIRI("https://schema.org/priceCurrency");
static IRI SCHEMA_ORG_CATEGORY = rdf.createIRI("https://schema.org/category");
// Added predicate for receipts
static IRI SCHEMA_ORG_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.
* - headers: The com.inrupt.client.Headers that contains HTTP header information.
* <p>
* In addition, the subject field is initialized.
*/
public Expense(final URI identifier, final Dataset dataset, final Headers headers) {
super(identifier, dataset, headers);
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_ORG_PROVIDER, ValueMappings::literalAsString);
}
void setMerchantProvider(String provider) {
overwriteNullable(SCHEMA_ORG_PROVIDER, provider, TermMappings::asStringLiteral);
}
public Date getExpenseDate() {
Instant expenseInstant = anyOrNull(SCHEMA_ORG_PURCHASE_DATE, ValueMappings::literalAsInstant);
if (expenseInstant != null) return Date.from(expenseInstant);
else return null;
}
public void setExpenseDate(Date expenseDate) {
overwriteNullable(SCHEMA_ORG_PURCHASE_DATE, expenseDate.toInstant(), TermMappings::asTypedLiteral);
}
String getDescription() {
return anyOrNull(SCHEMA_ORG_DESCRIPTION, ValueMappings::literalAsString);
}
void setDescription(String description) {
overwriteNullable(SCHEMA_ORG_DESCRIPTION, description, TermMappings::asStringLiteral);
}
public BigDecimal getAmount() {
String priceString = anyOrNull(SCHEMA_ORG_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_ORG_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_ORG_PRICE_CURRENCY, ValueMappings::literalAsString);
}
public void setCurrency(String currency) {
overwriteNullable(SCHEMA_ORG_PRICE_CURRENCY, currency, TermMappings::asStringLiteral);
}
public String getCategory() {
return anyOrNull(SCHEMA_ORG_CATEGORY, ValueMappings::literalAsString);
}
public void setCategory(String category) {
overwriteNullable(SCHEMA_ORG_CATEGORY, category, TermMappings::asStringLiteral);
}
// Node class: Added getter for receipts
public Set<String> getReceipts() {
return objects(SCHEMA_ORG_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.Headers
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 non-class-member fields
* when serializing Expense data as JSON.
*/
@JsonIgnoreProperties(value = [ "metadata", "headers", "graph", "graphNames", "entity", "contentType" ])
class Expense(
identifier: URI,
dataset: Dataset = rdf.createDataset(),
headers: Headers = Headers.empty(),
) : SolidRDFSource(identifier, dataset, headers) {
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_ORG_PURCHASE_DATE: IRI = rdf.createIRI("https://schema.org/purchaseDate")
var SCHEMA_ORG_PROVIDER: IRI = rdf.createIRI("https://schema.org/provider")
var SCHEMA_ORG_DESCRIPTION: IRI = rdf.createIRI("https://schema.org/description")
var SCHEMA_ORG_TOTAL_PRICE: IRI = rdf.createIRI("https://schema.org/totalPrice")
var SCHEMA_ORG_PRICE_CURRENCY: IRI = rdf.createIRI("https://schema.org/priceCurrency")
var SCHEMA_ORG_CATEGORY: IRI = rdf.createIRI("https://schema.org/category")
// Added predicate for receipts
var SCHEMA_ORG_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(), Headers.empty()) {
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(), Headers.empty())
/**
* 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_ORG_PROVIDER) { term: RDFTerm, graph: Graph ->
ValueMappings.literalAsString(term, graph)
}
set(provider) {
overwriteNullable(SCHEMA_ORG_PROVIDER, provider) { value: String?, graph: Graph ->
TermMappings.asStringLiteral(value, graph)
}
}
var expenseDate: Date?
get() {
val expenseInstant: Instant? = anyOrNull(SCHEMA_ORG_PURCHASE_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_ORG_PURCHASE_DATE, expenseInstant) { value: Instant?, graph: Graph ->
TermMappings.asTypedLiteral(value, graph)
}
}
var description: String?
get() = anyOrNull(SCHEMA_ORG_DESCRIPTION) { term: RDFTerm?, graph: Graph ->
ValueMappings.literalAsString(term, graph)
}
set(description) {
overwriteNullable(SCHEMA_ORG_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_ORG_TOTAL_PRICE) { term: RDFTerm?, graph: Graph ->
ValueMappings.literalAsString(term, graph)
}
return if (priceString != null) BigDecimal(priceString) else null
}
set(totalPrice) {
overwriteNullable(SCHEMA_ORG_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_ORG_PRICE_CURRENCY) { term: RDFTerm?, graph: Graph ->
ValueMappings.literalAsString(term, graph)
}
set(currency) {
overwriteNullable(SCHEMA_ORG_PRICE_CURRENCY, currency) { value: String?, graph: Graph ->
TermMappings.asStringLiteral(value, graph)
}
}
var category: String?
get() = anyOrNull(SCHEMA_ORG_CATEGORY) { term: RDFTerm?, graph: Graph ->
ValueMappings.literalAsString(term, graph)
}
set(category) {
overwriteNullable(SCHEMA_ORG_CATEGORY, category) { value: String?, graph: Graph ->
TermMappings.asStringLiteral(value, graph)
}
}
// Node class: Added getter for receipts
val receipts: MutableSet<String>
get() = objects(SCHEMA_ORG_IMAGE, TermMappings::asIri, ValueMappings::iriAsString)
// No setter added
}
}
ExpenseController
Class#
package com.example.gettingstarted;
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 com.inrupt.client.solid.PreconditionFailedException;
import com.inrupt.client.solid.ForbiddenException;
import com.inrupt.client.solid.NotFoundException;
import org.springframework.web.bind.annotation.*;
import org.apache.commons.rdf.api.RDFSyntax;
import com.inrupt.client.solid.SolidNonRDFSource;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URI;
import java.util.Set;
@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.getStorages();
}
}
/**
* 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 Expense createExpense(@RequestBody Expense newExpense) {
printWriter.println("ExpenseController:: createExpense");
try (var createdExpense = client.create(newExpense)) {
printExpenseAsTurtle(createdExpense);
return createdExpense;
} catch(PreconditionFailedException e1) {
// Errors if the resource already exists
printWriter.println(String.format("[%s] com.inrupt.client.solid.PreconditionFailedException:: %s", e1.getStatusCode(), e1.getMessage()));
} catch(ForbiddenException e2) {
// Errors if user does not have access to create
printWriter.println(String.format("[%s] com.inrupt.client.solid.ForbiddenException:: %s", e2.getStatusCode(), e2.getMessage()));
} catch(Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 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 (NotFoundException e1) {
// Errors if resource is not found
printWriter.println(String.format("[%s] com.inrupt.client.solid.NotFoundException:: %s", e1.getStatusCode(), e1.getMessage()));
} catch(ForbiddenException e2) {
// Errors if user does not have access to read
printWriter.println(String.format("[%s] com.inrupt.client.solid.ForbiddenException:: %s", e2.getStatusCode(), e2.getMessage()));
} catch(Exception e) {
e.printStackTrace();
}
return null;
}
/**
* Note 6: SolidSyncClient.update()
* Using the SolidSyncClient client.update() method,
* - Updates the Expense resource.
*/
@PutMapping("/expenses/update")
public Expense updateExpense(@RequestBody Expense expense) {
printWriter.println("ExpenseController:: updateExpense");
try(var updatedExpense = client.update(expense)) {
printExpenseAsTurtle(updatedExpense);
return updatedExpense;
} catch (NotFoundException e1) {
// Errors if resource is not found
printWriter.println(String.format("[%s] com.inrupt.client.solid.NotFoundException:: %s", e1.getStatusCode(), e1.getMessage()));
} catch(ForbiddenException e2) {
// Errors if user does not have access to read
printWriter.println(String.format("[%s] com.inrupt.client.solid.ForbiddenException:: %s", e2.getStatusCode(), e2.getMessage()));
} catch(Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 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 (NotFoundException e1) {
// Errors if resource is not found
printWriter.println(String.format("[%s] com.inrupt.client.solid.NotFoundException:: %s", e1.getStatusCode(), e1.getMessage()));
} catch(ForbiddenException e2) {
// Errors if user does not have access to read
printWriter.println(String.format("[%s] com.inrupt.client.solid.ForbiddenException:: %s", e2.getStatusCode(), e2.getMessage()));
} catch(Exception e) {
e.printStackTrace();
}
}
/**
* Note 8: Prints the expense resource in Turtle.
*/
private void printExpenseAsTurtle(Expense expense) {
printWriter.println("ExpenseController:: printExpenseAsTurtle");
ByteArrayOutputStream content = new ByteArrayOutputStream();
try {
expense.serialize(RDFSyntax.TURTLE, content);
printWriter.println(content.toString("UTF-8"));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Note 9: Stores a non-RDF resource to a Pod
*
* Using SolidNonRDFSource and the SolidSyncClient .create() method,
* - Saves a non-RDF resource at the destinationURL.
*/
@PutMapping("/resource/nonRDF/add")
public String 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()) {
SolidNonRDFSource myNonRDFFile = new SolidNonRDFSource(URI.create(destinationURL), file.getContentType(), fileStream);
return client.create(myNonRDFFile).getIdentifier().toString();
} catch(PreconditionFailedException e1) {
// Errors if the resource already exists
printWriter.println(String.format("[%s] com.inrupt.client.solid.PreconditionFailedException in addNonRDFFile:: %s", e1.getStatusCode(), e1.getMessage()));
} catch(ForbiddenException e2) {
// Errors if user does not have access to create
printWriter.println(String.format("[%s] com.inrupt.client.solid.ForbiddenException in addNonRDFFile:: %s", e2.getStatusCode(), e2.getMessage()));
} catch(Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 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 Expense 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.");
try {
String receiptLocation = addNonRDFFile(destinationURL, file);
if (receiptLocation != null) {
Expense expense = getExpense(expenseURL);
expense.addReceipt(receiptLocation);
return updateExpense(expense);
} else {
printWriter.println("Error adding receipt");
return null;
}
} catch(ForbiddenException e2) {
// Errors if user does not have access to read or update the Expense resource
printWriter.println(String.format("[%s] com.inrupt.client.solid.ForbiddenException in addReceiptToExpense:: %s", e2.getStatusCode(), e2.getMessage()));
} catch(Exception e) {
e.printStackTrace();
}
return null;
}
}
package com.example.gettingstarted
import com.inrupt.client.openid.OpenIdSession
import com.inrupt.client.solid.SolidSyncClient
import com.inrupt.client.webid.WebIdProfile
import com.inrupt.client.solid.PreconditionFailedException
import com.inrupt.client.solid.ForbiddenException
import com.inrupt.client.solid.NotFoundException
import org.springframework.web.bind.annotation.*
import java.io.PrintWriter
import java.net.URI
import com.inrupt.client.solid.SolidNonRDFSource
import org.springframework.web.multipart.MultipartFile
import org.apache.commons.rdf.api.RDFSyntax
import java.io.ByteArrayOutputStream
import java.io.IOException
@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.storages }
}
/**
* 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): Expense? {
printWriter.println("ExpenseController:: createExpense")
try {
val createdExpense = client.create(newExpense)
printExpenseAsTurtle(createdExpense)
return createdExpense
} catch(e1: PreconditionFailedException) {
// Errors if the resource already exists
printWriter.println(String.format("[%s] com.inrupt.client.solid.PreconditionFailedException:: %s", e1.statusCode, e1.localizedMessage))
} catch(e2: ForbiddenException) {
// Errors if user does not have access to create
printWriter.println(String.format("[%s] com.inrupt.client.solid.ForbiddenException:: %s", e2.statusCode, e2.localizedMessage))
} catch(e: Exception) {
e.printStackTrace()
}
return null
}
/**
* 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(e1: NotFoundException) {
// Errors if the resource is not found
printWriter.println(String.format("[%s] com.inrupt.client.solid.NotFoundException:: %s", e1.statusCode, e1.localizedMessage))
} catch(e2: ForbiddenException) {
// Errors if user does not have access to read
printWriter.println(String.format("[%s] com.inrupt.client.solid.ForbiddenException:: %s", e2.statusCode, e2.localizedMessage))
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
/**
* Note 6: SolidSyncClient.update()
* Using the SolidSyncClient client.update() method,
* - Updates the Expense resource.
*/
@PutMapping("/expenses/update")
fun updateExpense(@RequestBody expense: Expense): Expense? {
printWriter.println("ExpenseController:: updateExpense")
try {
val updatedExpense = client.update(expense)
printExpenseAsTurtle(updatedExpense)
return updatedExpense
} catch(e1: NotFoundException) {
// Errors if the resource is not found
printWriter.println(String.format("[%s] com.inrupt.client.solid.NotFoundException:: %s", e1.statusCode, e1.localizedMessage))
} catch(e2: ForbiddenException) {
// Errors if user does not have access to read
printWriter.println(String.format("[%s] com.inrupt.client.solid.ForbiddenException:: %s", e2.statusCode, e2.localizedMessage))
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
/**
* 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(Expense(URI.create(resourceURL)))
} catch(e1: NotFoundException) {
// Errors if the resource is not found
printWriter.println(String.format("[%s] com.inrupt.client.solid.NotFoundException:: %s", e1.statusCode, e1.localizedMessage))
} catch(e2: ForbiddenException) {
// Errors if user does not have access to read
printWriter.println(String.format("[%s] com.inrupt.client.solid.ForbiddenException:: %s", e2.statusCode, e2.localizedMessage))
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* Note 8: Prints the expense resource in Turtle.
*/
fun printExpenseAsTurtle(
expense: Expense
) {
printWriter.println("ExpenseController:: printExpenseAsTurtle")
val content = ByteArrayOutputStream()
try {
expense.serialize(RDFSyntax.TURTLE, content)
printWriter.println(content.toString("UTF-8"))
} catch (e: IOException) {
e.printStackTrace()
}
}
/**
* Note 9: Stores a non-RDF resource to a Pod
*
* Using SolidNonRDFSource and the SolidSyncClient's .create() method,
* - Saves a non-RDF resource at the destinationURL.
*/
@PutMapping("/resource/nonRDF/add")
fun addNonRDFFile(
@RequestParam(value = "destinationURL") destinationURL: String,
@RequestParam(value = "file") file: MultipartFile
): String? {
printWriter.println("In addNonRDFFile: Save Non-RDF File to Pod.")
try {
file.inputStream.use { fileStream ->
val myNonRDFFile: SolidNonRDFSource = SolidNonRDFSource(URI.create(destinationURL), file.contentType, fileStream)
return client.create(myNonRDFFile).identifier.toString()
}
} catch(e1: PreconditionFailedException) {
// Errors if the resource already exists
printWriter.println(String.format("[%s] com.inrupt.client.solid.PreconditionFailedException in addNonRDFFile:: %s", e1.statusCode, e1.localizedMessage))
} catch(e2: ForbiddenException) {
// Errors if user does not have access to create
printWriter.println(String.format("[%s] com.inrupt.client.solid.ForbiddenException in addNonRDFFile:: %s", e2.statusCode, e2.localizedMessage))
} catch (e: Exception) {
e.printStackTrace()
}
return ""
}
/**
* 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
): Expense? {
printWriter.println("In AddReceiptToExpense: Save Receipt File to Pod and Update Associated Expense.")
val receiptLocation = addNonRDFFile(destinationURL, file)
return if (!receiptLocation.isNullOrEmpty()) {
val expense = getExpense(expenseURL)
if (expense != null) {
expense.addReceipt(receiptLocation)
updateExpense(expense)
} else {
null
}
} else {
printWriter.println("Error adding receipt")
null
}
}
}