Querying

Carbon LDP resources can be queried using SPARQL, a W3C standard query language.

SPARQL has some similarities to SQL, the Structured Query Language for relational databases, but there are also fundamental differences. The main difference with SPARQL is that, instead of filtering results to get a desired outcome (like in SQL), you apply patterns that are tested over the data to retrieve results. These patterns are given along with query form to define the shape of the results.

There are four different query forms:

  • SELECT
    Returns the matched data in a table-like structure
  • ASK
    Returns true or false indicating whether data matched the pattern(s)
  • CONSTRUCT
    Restructures the matched data into a graph described by a specified template
  • DESCRIBE
    Returns a description of how the data is internally stored

A pattern is defined by using three elements:

  1. Resource: any document, fragment or named fragment referenced by a URI
  2. Property: the name or URI of a property of the resource
  3. Value: the value of the property

For example, if we wanted to match a document that represented a project, which has the property name with the value "Project X", we could use the pattern:

  1. Resource: projects/project-01/ (the project’s ID/URI)
  2. Property: name
  3. Value: "Project X"

This example pattern fully defines all of its elements. Each pattern element has the exact value we’re trying to match. But querying involves retrieving more data than we already have, so each pattern element can also be a variable.

A variable acts like a wildcard, matching any values the pattern element can have. But unlike wildcards, a variable stores any value it matches (you can think of them kind of like the columns requested on SQL queries).

E.g: To query the name of a project you would only need the project’s URI (resource) and the name (property). The value would be represented by a variable in which you will obtain the data you queried for.

Now, you may be asking: “what about modifying a document like UPDATE or ALTER?”. For those cases the platform also supports SPARQL UPDATE, another W3C standard, that lets you add or delete any data (really, ANY data). Because of this, SPARQL UPDATE is currently only available to the System Administrator, though we intend to broaden that scope in a subsequent release.

SPARQL Query Builder

The SDK integrates our own query builder called SPARQLER (SPARQL query buildER) which offers a fluent interface to help you construct queries and prevent errors by malformed patterns.

Currently, we only support SELECT queries with SPARQLER.
We intend to extend the builder to cover more features in a subsequent release.

 

To create a query you must call the method $sparql( documentURI:string ) of the documentsendpoint along with the document URI to query against.

If you already have a Document you can also call its $sparql() method, to query directly on it.

let carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "resource-end-point/" )
    // ... query construction
    ;


let document;
// ... get the persisted Document
document.$sparql()
    // ... query construction
    ;
import { Document } from "carbonldp/Document";

let carbonldp:CarbonLDP;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "resource-end-point/" )
    // ... query construction
    ;


let document:Document;
// ... get the persisted Document
document.$sparql()
    // ... query construction
    ;
var carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "resource-end-point/" )
    // ... query construction
    ;


var document;
// ... get the persisted Document
document.$sparql()
    // ... query construction
    ;

SELECT Query

General structure

SPARQL SELECT queries have the following structure and order:

  1. Select what data to include in the response:
    • .select( ... variables ) or .selectAll()
  2. Specify the patterns to match:
    • .where( ... patterns )
  3. Add optional solution modifiers:
    • .groupBy( ... )
    • .having( ... )
    • .orderBy( ... )
    • .limit( ... )
    • .offset( ... )
  4. And finally execute the query:
    • .execute()

Data Selection

We can specify which variables to be included by using one of two methods:

  • .select( ...variables:string[] ). Specify which variables used in the pattern(s) will be returned with the query result
  • .selectAll(). Indicate that all variables used in the pattern(s) will be included in the result

In the case of the pattern to match, it’s a little different since you need to provide a function where you can construct the pattern:

  • .where( patternFunction:( builder:PatternBuilder ) => GraphPattern ). A single pattern to match
  • .where( patternFunction:( builder:PatternBuilder ) => GraphPattern[] ). Multiple patterns to match

As you can see, the patternFunction receives a SPARQL/PatternBuilder object which contains helper properties and functions to construct the patterns you need.

Example

Assuming we had the following object stored:

// Representation of the stored object
{
        // Document URI
        "@id": "http://localhost:8083/projects/project-01/",

        // Properties
        "name": "Project X",
        "startDate": "2017/02/19"

        // ...
}

The following query would retrieve the values of the name and startDate property:

let carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "projects/project-01/" )
    .select( "projectName", "projectStart" )
    .where( _ => {
        return _.resource( "projects/project-01/" )
            .has( "name", _.var( "projectName" ) )
            .and( "startDate", _.var( "projectStart" ) );
    } )
    .execute()
    .then( ( results ) => {
        console.log( results );
    } );
import { PatternBuilder } from "sparqler/patterns";
import { SPARQLSelectResults } from "carbonldp/SPARQL";

let carbonldp:CarbonLDP;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "projects/project-01/" )
    .select( "projectName", "projectStart" )
    .where( ( _:PatternBuilder ) => {
        return _.resource( "projects/project-01/" )
            .has( "name", _.var( "projectName" ) )
            .and( "startDate", _.var( "projectStart" ) );
    } )
    .execute()
    .then( ( results:SPARQLSelectResults ) => {
        console.log( results );
    } );
var carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "projects/project-01/" )
    .select( "projectName", "projectStart" )
    .where( function( _ ) {
        return _.resource( "projects/project-01/" )
            .has( "name", _.var( "projectName" ) )
            .and( "startDate", _.var( "projectStart" ) );
    } )
    .execute()
    .then( function( results ) {
        console.log( results );
    } );

The query can be translated to:

  • Search for a resource that has the id projects/project-01/
  • This resource must have a property name with any value(s)
    • Store those values in the projectName variable
  • The resource must also have the property startDate with any value(s)
    • Store those values in the projectStart variable
  • Return me the values stored in the variables projectName and projectStart

In this example we are using the var( variableName ) function of the helper object _ to declare two variables:

  • projectName
  • projectStart

These variables must appear in the pattern to match so they can be assigned values.

In the .where() method, we use the PatternBuilder to first indicate we are targeting a resource with the URI: projects/project-01/, and as the URI is relative, it will be automatically resolved to http://localhost:8083/projects/project-01/ (assuming the platform’s host is http://localhost:8083/).

From this resource we call the has() method, to indicate that the resource must have a property called name, with value(s) bound to the variable projectName.

Next we call the and() method, stating that it also needs to have a property startDate, with value(s) bound to the variable projectStart.

Example

To retrieve the name and start date from all available projects, we need to consider the following:

  • The end-point of the query must be the container of the projects: projects/
  • The children of the container are under the property: http://www.w3.org/ns/ldp#contains
  • We need to bind every child document (project) and ask for its name and start date, as in the previous example
let carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "projects/" )
    .prefix( "ldp", "http://www.w3.org/ns/ldp#" )
    .select( "projectName", "projectStart" )
    .where( _ => {
        return [
            _.resource( "projects/" )
                .has( "ldp:contains", _.var( "everyProject" ) ),
            _.var( "everyProject" )
                .has( "name", _.var( "projectName" ) )
                .and( "startDate", _.var( "projectStart" ) )
        ];
    } )
    .execute()
    .then( ( results ) => {
        console.log( results );
    } );
import { PatternBuilder } from "sparqler/patterns";
import { SPARQLSelectResults } from "carbonldp/SPARQL";

let carbonldp:Carbon;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "projects/" )
    .prefix( "ldp", "http://www.w3.org/ns/ldp#" )
    .select( "projectName", "projectStart" )
    .where( ( _:PatternBuilder ) => {
        return [
            _.resource( "projects/" )
                .has( "ldp:contains", _.var( "everyProject" ) ),
            _.var( "everyProject" )
                .has( "name", _.var( "projectName" ) )
                .and( "startDate", _.var( "projectStart" ) )
        ];
    } )
    .execute()
    .then( ( results:SPARQLSelectResults ) => {
        console.log( results );
    } );
var carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "projects/" )
    .prefix( "ldp", "http://www.w3.org/ns/ldp#" )
    .select( "projectName", "projectStart" )
    .where( function( _ ) {
        return [
            _.resource( "projects/" )
                .has( "ldp:contains", _.var( "everyProject" ) ),
            _.var( "everyProject" )
                .has( "name", _.var( "projectName" ) )
                .and( "startDate", _.var( "projectStart" ) )
        ];
    } )
    .execute()
    .then( function( results ) {
        console.log( results );
    } );

As we can see, the major difference is that we return of an array of patterns. The first pattern matches the child documents from the projects/ container and binds them to the everyProject variable. With this variable we can then ask for the name and start date of every corresponding project.

Solution modifiers

This methods are optional and they let you modify the solutions returned:

  • .groupBy( rawCondition:string ). Group the data in order to calculate aggregated values for a solution
  • .having( rawCondition:string ). Filter grouped solution sets
  • .orderBy( rawCondition:string ). Establish the order of the a sequence of solutions
  • .limit( limit:string ). Restrict the number of solutions returned
  • .offset( offset:string ). Control where the solutions returned start from the overall set of them

Example

Considering the same case of retrieving the name and start date of every project, but with the solutions modifierswe’ll retrieve the last 10 projects that have been started.

let carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "projects/" )
    .prefix( "ldp", "http://www.w3.org/ns/ldp#" )
    .select( "projectName", "projectStart" )
    .where( _ => {
        return [
            _.resource( "projects/" )
                .has( "ldp:contains", _.var( "everyProject" ) ),
            _.var( "everyProject" )
                .has( "name", _.var( "projectName" ) )
                .and( "startDate", _.var( "projectStart" ) )
        ];
    } )
    .orderBy( "DESC( ?projectStart )" )
    .limit( 10 )
    .execute()
    .then( ( results ) => {
        console.log( results );
    } );
import { PatternBuilder } from "sparqler/patterns";
import { SPARQLSelectResults } from "carbonldp/SPARQL";

let carbonldp:CarbonLDP;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "projects/" )
    .prefix( "ldp", "http://www.w3.org/ns/ldp#" )
    .select( "projectName", "projectStart" )
    .where( ( _:PatternBuilder ) => {
        return [
            _.resource( "projects/" )
                .has( "ldp:contains", _.var( "everyProject" ) ),
            _.var( "everyProject" )
                .has( "name", _.var( "projectName" ) )
                .and( "startDate", _.var( "projectStart" ) )
        ];
    } )
    .orderBy( "DESC( ?projectStart )" )
    .limit( 10 )
    .execute()
    .then( ( results:SPARQLSelectResults ) => {
        console.log( results );
    } );
var carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "projects/" )
    .prefix( "ldp", "http://www.w3.org/ns/ldp#" )
    .select( "projectName", "projectStart" )
    .where( function( _ ) {
        return [
            _.resource( "projects/" )
                .has( "ldp:contains", _.var( "everyProject" ) ),
            _.var( "everyProject" )
                .has( "name", _.var( "projectName" ) )
                .and( "startDate", _.var( "projectStart" ) )
        ];
    } )
    .orderBy( "DESC( ?projectStart )" )
    .limit( 10 )
    .execute()
    .then( function( results ) => {
        console.log( results );
    } );

We use the .orderBy() method to specify a descending order for the results, based on the project start date, and we use the .limit() method to limit the returned solutions to 10. Notice that it is not required to use a question mark with the variables declared with the _.var() helper. However, the solution modifiers like .orderBy() use RAW strings, so we have to use a question mark to specify the variable (e.g. ?projectStart.)

Query execution

Until now we’ve only been building the query and nothing more, but the important thing is to execute it and use the solutions returned.

For a SELECT query we have the following method:

Example

Following the previous examples, we’ll execute the query we have been building and then return an array of projects with the data obtained.

let carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "projects/" )
    .prefix( "ldp", "http://www.w3.org/ns/ldp#" )
    .select( "projectName", "projectStart" )
    .where( _ => {
        return [
            _.resource( "projects/" )
                .has( "ldp:contains", _.var( "everyProject" ) ),
            _.var( "everyProject" )
                .has( "name", _.var( "projectName" ) )
                .and( "startDate", _.var( "projectStart" ) )
        ];
    } )
    .orderBy( "DESC( ?projectStart )" )
    .limit( 10 )
    .execute()
    .then( ( result ) => {
        return result
            .bindings
            .map( binding => ( {
                name: binding[ "projectName" ],
                startDate: binding[ "projectStart" ],
            } ) );
    } );
import { SPARQLSelectResults } from "carbonldp/SPARQL";
import { PatternBuilder } from "sparqler/patterns";

let carbonldp:CarbonLDP;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "projects/" )
    .prefix( "ldp", "http://www.w3.org/ns/ldp#" )
    .select( "projectName", "projectStart" )
    .where( ( _:PatternBuilder ) => {
        return [
            _.resource( "projects/" )
                .has( "ldp:contains", _.var( "everyProject" ) ),
            _.var( "everyProject" )
                .has( "name", _.var( "projectName" ) )
                .and( "startDate", _.var( "projectStart" ) )
        ];
    } )
    .orderBy( "DESC( ?projectStart )" )
    .limit( 10 )
    .execute()
    .then( ( result:SPARQLSelectResults ) => {
        return result
            .bindings
            .map( binding => ( {
                name: binding[ "projectName" ],
                startDate: binding[ "projectStart" ],
            } ) );
    } );
var carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "projects/" )
    .prefix( "ldp", "http://www.w3.org/ns/ldp#" )
    .select( "projectName", "projectStart" )
    .where( function( _ ) {
        return [
            _.resource( "projects/" )
                .has( "ldp:contains", _.var( "everyProject" ) ),
            _.var( "everyProject" )
                .has( "name", _.var( "projectName" ) )
                .and( "startDate", _.var( "projectStart" ) )
        ];
    } )
    .orderBy( "DESC( ?projectStart )" )
    .limit( 10 )
    .execute()
    .then( function( result ) {
        return result
            .bindings
            .map( function( binding ) {
                return {
                    name: binding[ "projectName" ],
                    startDate: binding[ "projectStart" ],
                }
            } );
    } );

The result object contains a bindings property with an array containing the solutions of the query. Every element in this array is an object that has the variable names as keys and each variable’s bound value.

Query configuration

With the SPARQLER integration, every query has access to the following default information:

  • Prefixes. Every prefix in the global schema
  • Vocabulary. The platform instance vocabulary to resolve property names, as in the Object Schema
  • Base. The URI where your platform instance lives, against which relative URIs will be resolved.

To configure the builder to your own preferences you can use the following methods:

  • vocab( uri:string ): Set or replace the existing vocabulary
  • base( uri:string ): Replace the base URI
  • prefix( name:string, uri:string ): Add or replace existing prefixes in the SPARQLER

Example

let carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "resource-end-point/" )
    // Sets a well defined vocabulary
    .vocab( "https://schema.org/" )

    // Sets the base to the document end point
    .base( "resource-end-point/" )

    // Adds new prefixes that may be used in the document
    .prefix( "bib", "https://bib.schema.org/" )
    .prefix( "auto", "https://auto.schema.org/" );
let carbonldp:CarbonLDP;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "resource-end-point/" )
    // Sets a well defined vocabulary
    .vocab( "https://schema.org/" )

    // Sets the base to the document end point
    .base( "resource-end-point/" )

    // Adds new prefixes that may be used in the document
    .prefix( "bib", "https://bib.schema.org/" )
    .prefix( "auto", "https://auto.schema.org/" );
var carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$sparql( "resource-end-point/" )
    // Sets a well defined vocabulary
    .vocab( "https://schema.org/" )

    // Sets the base to the document end point
    .base( "resource-end-point/" )

    // Adds new prefixes that may be used in the document
    .prefix( "bib", "https://bib.schema.org/" )
    .prefix( "auto", "https://auto.schema.org/" );

As an alternative to the way of building queries with the SPARQL Query Builder (as we’ve shown so far), the SDK also supports the use of standard SPARQL queries passed as strings. You might prefer this approach when if you are comfortable writing standard SPARQL queries or in order to leverage features that are not yet available in the SPARQL Query Builder.

Standard SPARQL queries can be written and tested using the SPARQL Client in the Carbon LDP Workbench. They can then be copied and pasted directly into JavaScript strings as-is. A notable difference is that relative property names cannot be used as they can in the SPARQL Query Builder, so standard SPARQL queries might be more verbose with fully qualified URIs in some cases.

The methods to execute a standard SPARQL query (given as a string), are also called from the Documents endpoint.

SELECT Queries

This method is the equivalent to the .execute() method in a SELECT query with the Query Builder.

let carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$executeSELECTQuery( "projects/", `
    BASE <http://localhost:8083/>
    PREFIX ldp: <http://www.w3.org/ns/ldp#>
    SELECT ?projectName ?projectStart
    WHERE {
        <projects/> ldp:contains ?everyProject .
        ?everyProject <vocabularies/main/#name> ?projectName ;
                      <vocabularies/main/#startDate> ?projectStart
    }
    ORDER BY DESC( ?projectStart )
    LIMIT 10
` )
.then( ( result ) => {
    return result
        .bindings
        .map( binding => ( {
                name: binding[ "projectName" ],
                startDate: binding[ "projectStart" ],
            } )
        );
} );
import { SPARQLSelectResults } from "carbonldp/SPARQL";

let carbonldp:CarbonLDP;

// ... initialize your CarbonLDP object

carbonldp.documents.$executeSELECTQuery( "projects/", `
    BASE <http://localhost:8083/>
    PREFIX ldp: <http://www.w3.org/ns/ldp#>
    SELECT ?projectName ?projectStart
    WHERE {
        <projects/> ldp:contains ?everyProject .
        ?everyProject <vocabularies/main/#name> ?projectName ;
                      <vocabularies/main/#startDate> ?projectStart
    }
    ORDER BY DESC( ?projectStart )
    LIMIT 10
` )
.then( ( result:SPARQLSelectResults ) => {
    return result
        .bindings
        .map( binding => ( {
                name: binding[ "projectName" ],
                startDate: binding[ "projectStart" ],
            } )
        );
} );
var carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$executeSELECTQuery( "projects/", "" +
    "BASE <http://localhost:8083/>" +
    "PREFIX ldp: <http://www.w3.org/ns/ldp#>" +
    "SELECT ?projectName ?projectStart" +
    "WHERE {" +
    "   <projects/> ldp:contains ?everyProject ." +
    "   ?everyProject <vocabularies/main/#name> ?projectName ;" +
    "                 <vocabularies/main/#startDate> ?projectStart" +
    "}" +
    "ORDER BY DESC( ?projectStart )" +
    "LIMIT 10"
)
.then( function( result ) {
    return result
        .bindings
        .map( function( binding ) {
            return {
                name: binding[ "projectName" ],
                startDate: binding[ "projectStart" ],
            };
        } );
} );

ASK Queries

A standard SPARQL ASK query can be given as string and executed with the following:

This option returns a simple response with only the resulting boolean of the ASK query (e.g. true or false).

let carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$executeASKQuery( "projects/project-01/", `
    BASE        <http://localhost:8083/>
    ASK {
        <projects/project-01/> <vocabularies/main/#name> "Project X" ;
                               <vocabularies/main/#startDate> "2017/02/19"
    }
` )
    .then( ( result ) => {
        // result will be `true`
    } );
let carbonldp:CarbonLDP;

// ... initialize your CarbonLDP object

carbonldp.documents.$executeASKQuery( "projects/project-01/", `
    BASE        <http://localhost:8083/>
    ASK {
        <projects/project-01/> <vocabularies/main/#name> "Project X" ;
                               <vocabularies/main/#startDate> "2017/02/19"
    }
` )
    .then( ( result:boolean ) => {
        // result will be `true`
    } );
var carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$executeASKQuery( "projects/project-01/", "" +
    "BASE        <http://localhost:8083/>" +
    "ASK {" +
    "    <projects/project-01/> <vocabularies/main/#name> \"Project X\" ;" +
    "                           <vocabularies/main/#startDate> \"2017/02/19\"" +
    "}" +
)
    .then( function( result ) {
        // result will be `true`
    } );

UPDATE

A SPARQL UPDATE doesn’t return any results data, thus it’s not exactly a query and there’s only one method in the SDK for executing the UPDATE.

let carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$executeUPDATE( "projects/project-01/", `
    BASE     <http://localhost:8083/>
    DELETE {
        <projects/project-01/> <vocabularies/main/#name> "Project X"
    }
    INSERT {
        <projects/project-01/> <vocabularies/main/#name> "My new awesome name"
    }
    WHERE {
        <projects/project-01/> <vocabularies/main/#name> "Project X"
    }
` )
    .then( ()  => {
        // ... Update finished
    } );
let carbonldp:CarbonLDP;

// ... initialize your CarbonLDP object

carbonldp.documents.$executeUPDATE( "projects/project-01/", `
    BASE     <http://localhost:8083/>
    DELETE {
        <projects/project-01/> <vocabularies/main/#name> "Project X"
    }
    INSERT {
        <projects/project-01/> <vocabularies/main/#name> "My new awesome name"
    }
    WHERE {
        <projects/project-01/> <vocabularies/main/#name> "Project X"
    }
` )
    .then( () => {
        // ... Update finished
    } );
var carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$executeUPDATE( "projects/project-01/", "" +
    "BASE     <http://localhost:8083/>" +
    "DELETE {" +
    "	<projects/project-01/> <vocabularies/main/#name> \"Project X\"" +
    "}" +
    "INSERT {" +
    "	<projects/project-01/> <vocabularies/main/#name> \"My new awesome name\"" +
    "}" +
    "WHERE {" +
    "	<projects/project-01/> <vocabularies/main/#name> \"Project X\"" +
    "}"
)
    .then( function() {
        // ... Update finished
    } );

Raw queries

Finally, we understand that you may want to use SPARQL methods that are not available through the documents endpoint, or you may want to access information contained in external domains. In order to do this, you can use the following methods of
the SPARQLService:

  • executeRawSelectQuery( documentURI:string, rawQuery: string )
  • executeRawAskQuery( documentURI:string, rawQuery: string )
  • executeRawConstructQuery( documentURI:string, rawQuery: string )
  • executeRawDescribeQuery( documentURI:string, rawQuery: string )

Along with the regular Response, the SELECT and ASK methods will return a SPARQLRawResults object, while the CONSTRUCT and DESCRIBE will return an un-parsed String, which you can then use at your convenience.

Conclusion

In this guide, we’ve demonstrated two alternative ways to query with SPARQL using the SDK:

  • Using the SPARQL Query Builder (a.k.a. SPARQLER) – a fluent chain of methods assembled in a dot-notation to build up a query.
  • Using SPARQL Services – methods that accept a standard SPARQL query string passed in a parameter to the method.

We also provided an overview, with code examples, for the different query forms SELECT, ASK and UPDATE.

Want to know more?

Now that you understand how to query with SPARQL using the SDK, you may want to learn more about the SPARQL language. We recommend the following references:

RDF Icon carbonldp – for general discussions related to using Carbon LDP (Platform, REST API, product documentation)

RDF Icon SPARQL 1.1 Overview – Introduction from the W3C.

RDF Icon SPARQL 1.1 Query Language – Specification from the W3C.

RDF Icon Learning SPARQL – Book about Querying and Updating with SPARQL 1.1, by Bob DuCharme, O’REILLY®.