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
}
}
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;
}
}
Last updated