Freemarker templates are the heart of QTT, transforming simple requests into complex SPARQL queries.
Freemarker is a template engine that processes directives and expressions to generate text output. In QTT, templates generate SPARQL queries.
PREFIX declarations
CONSTRUCT or SELECT {
template content
}
WHERE {
query patterns
<#if parameter??>
conditional SPARQL
</#if>
}
<#if limit??>
LIMIT ${limit}
</#if>1. Parameter Access: ${paramName}
FILTER(CONTAINS(?name, "${searchTerm}"))2. Conditional Blocks: <#if>...</#if>
<#if startDate??>
FILTER(?date >= "${startDate}"^^xsd:date)
</#if>3. Parameter Existence Check: paramName??
<#if email??>
?person foaf:mbox "${email}" .
</#if>4. String Transformations
${name?lower_case} # Convert to lowercase
${name?upper_case} # Convert to uppercase
${name?trim} # Remove whitespace
${name?url} # URL encode5. Default Values
${limit!'10'} # Use '10' if limit not providedPREFIX foaf: <http://xmlns.com/foaf/0.1/>
CONSTRUCT {
?person foaf:name ?name .
}
WHERE {
?person a foaf:Person ;
foaf:name ?name .
<#if searchTerm??>
FILTER(CONTAINS(LCASE(STR(?name)), "${searchTerm?lower_case}"))
</#if>
}
<#if limit??>
LIMIT ${limit}
</#if>Usage:
# All people
GET /people-search
# Filtered search
GET /people-search?searchTerm=john&limit=5PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
CONSTRUCT {
?doc ex:title ?title ;
ex:author ?author ;
ex:date ?date .
}
WHERE {
?doc a ex:Document ;
ex:title ?title .
<#if author??>
?doc ex:author ?author .
FILTER(CONTAINS(LCASE(STR(?author)), "${author?lower_case}"))
</#if>
<#if startDate??>
?doc ex:date ?date .
FILTER(?date >= "${startDate}"^^xsd:date)
</#if>
<#if endDate??>
?doc ex:date ?date .
FILTER(?date <= "${endDate}"^^xsd:date)
</#if>
}
<#if limit??>
LIMIT ${limit}
</#if>PREFIX ex: <http://example.org/>
CONSTRUCT {
?item ex:name ?name ;
ex:type ?type .
}
WHERE {
?item a ex:Item ;
ex:name ?name ;
ex:type ?type .
<#if types??>
<#assign typeList = types?split(",")>
FILTER(?type IN (<#list typeList as type>"${type}"<#if type_has_next>, </#if></#list>))
</#if>
}Usage:
GET /items?types=TypeA,TypeB,TypeCPREFIX foaf: <http://xmlns.com/foaf/0.1/>
CONSTRUCT {
?person foaf:name ?name .
<#if includeEmail?? && includeEmail == "true">
?person foaf:mbox ?email .
</#if>
<#if includePhone?? && includePhone == "true">
?person foaf:phone ?phone .
</#if>
}
WHERE {
?person a foaf:Person ;
foaf:name ?name .
<#if includeEmail?? && includeEmail == "true">
OPTIONAL { ?person foaf:mbox ?email . }
</#if>
<#if includePhone?? && includePhone == "true">
OPTIONAL { ?person foaf:phone ?phone . }
</#if>
}The Monaco editor provides context-aware autocomplete for ontology elements.
Trigger Autocomplete:
- Type any character in the editor
- Press
Ctrl+Space
Autocomplete Provides:
- Classes: All OWL/RDFS classes from your GraphMart
- Properties: Object properties, datatype properties, annotation properties
- Individuals: Named instances
Example:
Type: PREFIX foaf: <http://xmlns.com/foaf/
[Autocomplete suggests URIs]
Type: ?person a foaf:
[Autocomplete suggests: Person, Organization, Agent, etc.]
Type: ?person foaf:
[Autocomplete suggests: name, mbox, knows, member, etc.]
Parameters from URL query string are automatically available:
GET /my-route?name=John&age=30Template access:
${headers.name} # "John"
${headers.age} # "30"Parameters from JSON body are automatically extracted:
POST /my-route
Content-Type: application/json
{
"name": "John",
"age": 30
}Template access (same as GET):
${body.name} # "John"
${body.age} # 30All parameters are strings by default. For type-safe queries:
# Integer
FILTER(?age = ${age})
# String (needs quotes)
FILTER(?name = "${name}")
# Date (with type cast)
FILTER(?date >= "${startDate}"^^xsd:date)
# Boolean
<#if includeInactive == "true">
# Include inactive items
</#if><#if !limit?? || limit?number < 1 || limit?number > 1000>
<#assign limit = "100">
</#if>
LIMIT ${limit}Freemarker automatically escapes strings, but be cautious with direct injection:
# GOOD - Freemarker handles escaping
FILTER(CONTAINS(?name, "${searchTerm}"))
# BAD - Direct injection risk
FILTER(CONTAINS(?name, ${searchTerm})) # Missing quotes!# Efficient - only includes OPTIONAL when needed
<#if includeEmail??>
OPTIONAL { ?person foaf:mbox ?email . }
</#if>
# Inefficient - always includes OPTIONAL
OPTIONAL { ?person foaf:mbox ?email . }<#assign maxResults = limit!'50'>
<#assign offset = offset!'0'>
LIMIT ${maxResults}
OFFSET ${offset}#
# Route: people-search
# Description: Searches for people by name, email, or organization
#
# Parameters:
# - name (optional): Person name filter
# - email (optional): Email address filter
# - org (optional): Organization name filter
# - limit (optional, default=50): Maximum results
#
# Example: /people-search?name=john&limit=10
#
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
...- Start with a basic CONSTRUCT query without parameters
- Test manually with curl
- Add one parameter at a time
- Test each addition
- Use SPARQi to validate complex logic
# Use specific graph patterns
?person a foaf:Person . # More specific than ?person a ?type
# Limit early in complex queries
{
SELECT DISTINCT ?person WHERE {
?person a foaf:Person .
<#if name??>
?person foaf:name ?name .
FILTER(CONTAINS(?name, "${name}"))
</#if>
}
LIMIT ${limit!'100'}
}
# Use OPTIONAL sparingly
OPTIONAL { ?person foaf:mbox ?email . } # Only if truly optionalCheck Generated SPARQL:
Enable Karaf logging to see the generated SPARQL queries:
# In Karaf console
log:set DEBUG com.inovexcorp.queryservice
# View logs
docker logs -f qttCommon Issues:
-
Missing quotes around string parameters
# Wrong FILTER(?name = ${name}) # Correct FILTER(?name = "${name}")
-
Unclosed Freemarker directives
# Wrong <#if param??> FILTER clause # Missing </#if> # Correct <#if param??> FILTER clause </#if>
-
Type mismatches
# Wrong - limit is a string LIMIT ${limit} # Correct - convert to number if validating <#assign limitNum = limit?number> LIMIT ${limitNum}
When using Redis caching, template design affects cache efficiency:
Cache Key Generation:
- Cache keys are generated from the final SPARQL query + GraphMart + Layers
- Different parameters create different cache keys
- Case-sensitive:
?name=Johnand?name=johnare different keys
Optimizing Cache Hit Rate:
Normalize parameters in your template:
<#-- Normalize to lowercase for better cache hit rate -->
<#assign normalizedName = (name!"")?lower_case>
SELECT * WHERE {
?person foaf:name ?name .
FILTER(LCASE(?name) = "${normalizedName}")
}Now both ?name=John and ?name=john generate the same SPARQL query, resulting in the same cache key.