Step 2: Expense Class#
In this tutorial, an expense record (with date, description, provider, amount, currency, category information) is stored as an RDF (Resource Description Framework) file. For example, a saved expense RDF file may contain the following content (shown in Turtle format):
<https://storage.example.com/{rootContainer}/expenses/20230306/teamLunchExpense>
<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://schema.org/Invoice> ;
<https://schema.org/purchaseDate> "2023-03-07T00:00:00Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> ;
<https://schema.org/provider> "Example Restaurant" ;
<https://schema.org/description> "Team Lunch";
<https://schema.org/category> "Travel and Entertainment" ;
<https://schema.org/priceCurrency> "USD" ;
<https://schema.org/totalPrice> "120"^^<http://www.w3.org/2001/XMLSchema#decimal> .
Note
In addition to the expense data, the triples also include an RDF type statement, which acts to describe the resource as a whole; in this example, an https://schema.org/Invoice.
Create the Expense
Class#
Tip
Various aspects related to modeling a Solid RDF Resource are noted as comments in the code. For more details, see Data Modeling (RDF).
In the src/main/java/com/example/gettingstarted/
directory,
create an Expense.java
class with the following content:
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;
/**
* 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");
/**
* 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);
}
@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) {
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);
}
/**
* 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);
}
/**
* 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);
}
}
}
In the src/main/kotlin/com/example/gettingstarted/
directory,
create an Expense.kt
Kotlin class with the following content:
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.*
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")
/**
* 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)
/**
* 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 corresponds to the resource.
* - headers: The com.inrupt.client.Headers that contains header information.
*
*
* In addition, the subject field is initialized.
*/
@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?
) : 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
}
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
}
/**
* 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)
}
}
}
}
Additional Information#
For more information, see: