/* See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* Esri Inc. licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.esri.gpt.catalog.discovery.rest;
import com.esri.gpt.catalog.context.CatalogConfiguration;
import com.esri.gpt.catalog.discovery.*;
import com.esri.gpt.framework.context.ConfigurationException;
import com.esri.gpt.framework.context.RequestContext;
import com.esri.gpt.framework.util.Val;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
/**
* Provides functionality to parse a rest query URL.
* <p/>
* The primary goal is to build filter and response components associated
* with the query.
*/
public class RestQueryParser {
/** instance variables ====================================================== */
private AliasedDiscoverables discoverables;
private RequestContext requestContext;
private Map<String, String[]> requestParameterMap;
private RestQuery restQuery;
/** constructors ============================================================ */
/**
* Constructs the parser.
* @param request the active HTTP request
* @param context the request context
* @param query the query to populate
*/
public RestQueryParser(HttpServletRequest request, RequestContext context, RestQuery query) {
this.requestParameterMap = request.getParameterMap();
this.requestContext = context;
this.restQuery = query;
// determine the aliased discoverables
CatalogConfiguration catCfg = this.requestContext.getCatalogConfiguration();
PropertyMeanings propertyMeanings = catCfg.getConfiguredSchemas().getPropertyMeanings();
setDiscoverables(propertyMeanings.getDcPropertySets().getAllAliased());
// establish RSS provider and source URLs
String basePath = RequestContext.resolveBaseContextPath(request);
getQuery().setRssProviderUrl(catCfg.getParameters().getValue("rssProviderUrl"));
if (getQuery().getRssProviderUrl().length() == 0) {
getQuery().setRssProviderUrl(basePath);
}
String sourceURL = basePath+"/rest/find/document";
String queryString = request.getQueryString();
getQuery().setRssSourceUrl(sourceURL + (queryString != null? "?"+queryString: ""));
getQuery().setMoreUrl(sourceURL + "?" + getMoreQueryString(Val.chkStr(queryString)));
}
/** properties ============================================================== */
/**
* Gets the aliased map of configured discoverable properties.
* @return the discoverables
*/
public AliasedDiscoverables getDiscoverables() {
return this.discoverables;
}
/**
* Sets the aliased map of configured discoverable properties.
* @param discoverables the discoverables
*/
public void setDiscoverables(AliasedDiscoverables discoverables) {
this.discoverables = discoverables;
}
/**
* Convenience method to return the filter associated with the query being populated.
* @return the filter
*/
public DiscoveryFilter getFilter() {
return getQuery().getFilter();
}
/**
* Gets the query being populated.
* @return the query
*/
public RestQuery getQuery() {
return this.restQuery;
}
/** methods ================================================================= */
/**
* Appends a clause to the query filter.
* <br/>The clause will not be appended if it or it's parent is null.
* @param parent the parent to which the supplied discovery clause will be appended
* @param clause the clause to append
*/
public void appendClause(LogicalClause parent, DiscoveryClause clause) {
if ((parent != null) && (clause != null)) {
parent.getClauses().add(clause);
}
}
/**
* Extracts a property clause from the HTTP request.
* <br/>This method checks the HTTP request parameter map for the value associated with a
* supplied key. If found, the supplied PropertyClause is populated with an appropriate
* literal and target, then returned.
* @param clause the property clause to populate and return
* @param restKey the URL key for the parameter
* @param discoverableKey the key associated with the target discoverable
* @return the property clause (null if not validly located within the request)
*/
public PropertyClause extractProperty(PropertyClause clause,
String restKey,
String discoverableKey) {
if ((clause == null) ||
(clause instanceof PropertyClause.PropertyIsNull) ||
(clause instanceof PropertyClause.PropertyIsBetween)) {
return null; // can't be handled
}
String value = Val.chkStr(getRequestParameter(restKey));
if(restKey.equals("q") && discoverableKey.equals("anytext")){
if(value.equalsIgnoreCase("(type:tool%20OR%20type:toolset%20OR%20type:toolbox)")){
value= "";
}
}
Discoverable target = findDiscoverable(discoverableKey);
if (target != null) {
clause.setTarget(target);
if ((value != null) && (value.length() > 0)) {
try {
clause.getTarget().getMeaning().getValueType().evaluate(value);
} catch (Exception e) {
// TODO Log warning??, throw exception ??
value = null;
}
}
clause.setLiteral(value);
String literal = clause.getLiteral();
if ((literal != null) && (literal.length() > 0)) {
if (clause instanceof PropertyClause.PropertyIsEqualTo) {
} else if (clause instanceof PropertyClause.PropertyIsGreaterThan) {
} else if (clause instanceof PropertyClause.PropertyIsGreaterThanOrEqualTo) {
} else if (clause instanceof PropertyClause.PropertyIsLessThan) {
} else if (clause instanceof PropertyClause.PropertyIsLessThanOrEqualTo) {
} else if (clause instanceof PropertyClause.PropertyIsLike) {
} else if (clause instanceof PropertyClause.PropertyIsNotEqualTo) {
} else {
return null;
}
return clause;
}
}
/*
private Date extractDate(HttpServletRequest request, String key) {
Date date = null;
String sDate = extractParameter(request, key);
if (sDate.length() > 0) {
try {
date = DF.parse(sDate);
} catch (ParseException ex) {
LogUtil.getLogger().info("Invalid date: "+sDate);
}
}
return date;
*/
return null;
}
/**
* Extracts a grouping of property clauses (delimited list) from the HTTP request.
* <br/>This method checks the HTTP request parameter map for the delimited value
* associated with a supplied key.
* <br/>If found, the delimited value is tokenized into a collection of "equal to"
* property clauses ({@link com.esri.gpt.catalog.discovery.PropertyClause.PropertyIsEqualTo}).
* <br/>If multiple values are found, they will be logically grouped based upon the
* supplied "orBased" parameter ({@link LogicalClause}).
* @param restKey the URL key for the parameter
* @param discoverableKey the key associated with the target discoverable
* @param delimiter the value delimiter
* @param orBased if true, group within a logical or, otherwise group within a logical and
* @return an appropriate discovery clause (null if not validly located within the request)
*/
public DiscoveryClause extractPropertyList(String restKey,
String discoverableKey,
String delimiter,
boolean orBased) {
Discoverable discoverable = findDiscoverable(discoverableKey);
String delimitedValues = getRequestParameter(restKey);
String[] values = Val.tokenize(delimitedValues,delimiter);
if ((discoverable != null) && (values != null) && (values.length > 0)) {
LogicalClause logical = null;
if (orBased) {
logical = new LogicalClause.LogicalOr();
} else {
logical = new LogicalClause.LogicalAnd();
}
for (String value: values) {
value = Val.chkStr(value);
if (value.length() > 0) {
PropertyClause clause = new PropertyClause.PropertyIsEqualTo();
clause.setTarget(discoverable);
clause.setLiteral(value);
logical.getClauses().add(clause);
}
}
if (logical.getClauses().size() > 1) {
return logical;
} else if (logical.getClauses().size() == 1) {
return logical.getClauses().get(0);
}
}
return null;
}
/**
* Extracts a range based discovery clause from the HTTP request.
* <br/>This method checks the HTTP request parameter map for the lower and/or upper
* boundaries of a range based query.
* <br/>If found, the lower boundary will be associated with a
* {@link com.esri.gpt.catalog.discovery.PropertyClause.PropertyIsGreaterThanOrEqualTo} clause, the upper boundary with a
* {@link com.esri.gpt.catalog.discovery.PropertyClause.PropertyIsLessThanOrEqualTo} clause.
* <br/>If both are found, a {@link com.esri.gpt.catalog.discovery.LogicalClause.LogicalAnd} clause is returned.
* @param restLowerKey the URL key for the lower value parameter of the range
* @param restUpperKey the URL key for the upper value parameter of the range
* @param discoverableKey the key associated with the target discoverable
* @return an appropriate discovery clause (null if not validly located within the request)
*/
public DiscoveryClause extractPropertyRange(String restLowerKey,
String restUpperKey,
String discoverableKey) {
PropertyClause lower = this.extractProperty(
new PropertyClause.PropertyIsGreaterThanOrEqualTo(),restLowerKey,discoverableKey);
PropertyClause upper = this.extractProperty(
new PropertyClause.PropertyIsLessThanOrEqualTo(),restUpperKey,discoverableKey);
// this isn't ideal, but it's here to handle a circumstance where
// a validBefore URL parameter has been passed as validTo
if (upper == null) {
if ((restLowerKey != null) && restLowerKey.equalsIgnoreCase("validAfter")) {
if ((restUpperKey != null) && restUpperKey.equalsIgnoreCase("validBefore")) {
upper = this.extractProperty(
new PropertyClause.PropertyIsLessThanOrEqualTo(),"validTo",discoverableKey);
}
}
}
if ((lower != null) && (upper != null)) {
LogicalClause logical = new LogicalClause.LogicalAnd();
logical.getClauses().add(lower);
logical.getClauses().add(upper);
return logical;
} else if (lower != null) {
return lower;
} else if (upper != null) {
return upper;
}
return null;
}
/**
* Extracts sort option parameters from the HTTP request.
* <br/>This method checks the HTTP request parameter map for an order by
* parameter and constructs an appropriate Sortables component if located.
* @param restKey the URL key for the sort option parameter
* @return the sortables (null if not validly located within the request)
*/
public Sortables extractSortables(String restKey) {
Sortables sortables = null;
Discoverable target = null;
Sortable.SortDirection direction = null;
String sortBy = Val.chkStr(getRequestParameter(restKey));
if (sortBy.equalsIgnoreCase("dateAscending")) {
target = this.findDiscoverable("dct:modified");
direction = Sortable.SortDirection.ASC;
} else if (sortBy.equalsIgnoreCase("dateDescending")) {
target = this.findDiscoverable("dct:modified");
direction = Sortable.SortDirection.DESC;
} else if (sortBy.equalsIgnoreCase("relevance")) {
} else if (sortBy.equalsIgnoreCase("title")) {
target = this.findDiscoverable("dc:title");
direction = Sortable.SortDirection.ASC;
} else if (sortBy.equalsIgnoreCase("format")) {
target = this.findDiscoverable("contentType");
direction = Sortable.SortDirection.ASC;
} else if (sortBy.equalsIgnoreCase("areaAscending")) {
target = this.findDiscoverable("geometry");
direction = Sortable.SortDirection.ASC;
} else if (sortBy.equalsIgnoreCase("areaDescending")) {
target = this.findDiscoverable("geometry");
direction = Sortable.SortDirection.DESC;
} else {
if (sortBy.length() > 0) {
if (sortBy.toLowerCase().endsWith(".asc")) {
target = this.findDiscoverable(sortBy.substring(0,sortBy.length()-4));
direction = Sortable.SortDirection.ASC;
} else if (sortBy.toLowerCase().endsWith(".ascending")) {
target = this.findDiscoverable(sortBy.substring(0,sortBy.length()-10));
direction = Sortable.SortDirection.ASC;
} else if (sortBy.toLowerCase().endsWith(".desc")) {
target = this.findDiscoverable(sortBy.substring(0,sortBy.length()-5));
direction = Sortable.SortDirection.DESC;
} else if (sortBy.toLowerCase().endsWith(".descending")) {
target = this.findDiscoverable(sortBy.substring(0,sortBy.length()-11));
direction = Sortable.SortDirection.DESC;
} else {
target = this.findDiscoverable(sortBy);
direction = Sortable.SortDirection.ASC;
}
}
}
if ((target != null) && (direction != null)) {
Sortable sortable = target.asSortable();
sortable.setDirection(direction);
sortables = new Sortables();
sortables.add(sortable);
}
return sortables;
}
/**
* Extracts the spatial clause from the HTTP request.
* <br/>This method checks the HTTP request parameter map for spatial related
* parameters and constructs a SpatialClause if located (filter related).
* @param restBBoxKey the URL key for the BBOX parameter
* @param restOperatorKey the URL key for the spatial operator parameter
* @param discoverableKey the key associated with the target discoverable
* @return the spatial clause (null if not validly located within the request)
*/
public SpatialClause extractSpatialClause(String restBBoxKey,
String restOperatorKey,
String discoverableKey) {
// make the clause
SpatialClause clause = null;
String operator = Val.chkStr(getRequestParameter(restOperatorKey));
if (operator.equalsIgnoreCase("BBOX")) {
clause = new SpatialClause.GeometryBBOXIntersects();
} else if (operator.equalsIgnoreCase("Contains")) {
clause = new SpatialClause.GeometryContains();
} else if (operator.equalsIgnoreCase("Disjoint")) {
clause = new SpatialClause.GeometryIsDisjointTo();
} else if (operator.equalsIgnoreCase("Equals")) {
clause = new SpatialClause.GeometryIsEqualTo();
} else if (operator.equalsIgnoreCase("Intersects")) {
clause = new SpatialClause.GeometryIntersects();
} else if (operator.equalsIgnoreCase("Overlaps")) {
clause = new SpatialClause.GeometryOverlaps();
} else if (operator.equalsIgnoreCase("Within")) {
clause = new SpatialClause.GeometryIsWithin();
} else if (operator.equalsIgnoreCase("esriSpatialRelWithin")) {
clause = new SpatialClause.GeometryIsWithin();
} else if (operator.equalsIgnoreCase("esriSpatialRelOverlaps")) {
clause = new SpatialClause.GeometryOverlaps();
} else {
clause = new SpatialClause.GeometryBBOXIntersects();
}
// determine the target and envelope, check before returning
Discoverable target = findDiscoverable(discoverableKey);
if ((clause != null) && (target != null)) {
clause.setTarget(target);
String sBBox = Val.chkStr(getRequestParameter(restBBoxKey));
String[] aBBox = sBBox.split(",");
if (aBBox.length == 4) {
clause.getBoundingEnvelope().put(aBBox[0],aBBox[1],aBBox[2],aBBox[3]);
}
if (!clause.getBoundingEnvelope().isEmpty()) {
return clause;
}
}
return null;
}
/**
* Finds the discoverable property associated with a discoverable key.
* @param discoverableKey the discoverable key (or alias)
* @return the discoverable property (null if none was found);
*/
public Discoverable findDiscoverable(String discoverableKey) {
return getDiscoverables().get(discoverableKey);
}
/**
* Instantiates property clause from a supplied property clause class name.
* <br/>A runtime exception will be thrown if the supplien class name is invalid.
* @param class name the class name of the property clause to instantiate
* @return the new property clause object
*/
private PropertyClause makePropertyClause(String className) {
try {
Class<?>cls = Class.forName(className);
Object obj = cls.newInstance();
if (obj instanceof PropertyClause) {
return (PropertyClause)obj;
} else {
String sMsg = "The configured PropertyClause class name is invalid: "+className;
throw new ConfigurationException(sMsg);
}
} catch (ConfigurationException t) {
throw t;
} catch (Throwable t) {
String sMsg = "Error instantiating PropertyClause: "+className;
throw new ConfigurationException(sMsg,t);
}
}
/**
* Gets the HTTP request parameter value associated with a key.
* @param parameterKey the parameter key
* @return the parameter value (empty string if not found)
*/
public String getRequestParameter(String parameterKey) {
for (Map.Entry<String, String[]> e : this.requestParameterMap.entrySet()) {
if (e.getKey().equalsIgnoreCase(parameterKey)) {
if (e.getValue().length > 0) {
return Val.chkStr(e.getValue()[0]);
} else {
return "";
}
}
}
return "";
}
/*
// This is just an example of usage, for an implementation see:
// {@link com.esri.gpt.control.georss.RestQueryServlet#parseRequest(HttpServletRequest, RequestContext)}
private void parse() {
parseResponseFormat("f");
parseResponseGeometry("geometryType");
parseResponseStyle("style");
parseResponseTarget("target");
parseStartRecord("start",1);
parseMaxRecords("max",10);
parsePropertyIsEqualTo("uuid","uuid");
parsePropertyIsLike("searchText","anytext");
parsePropertyList("contentType","dc:type",",",true);
parsePropertyList("dataCategory","dc:subject",",",true);
parsePropertyRange("after","before","dct:modified");
parseSpatialClause("bbox","spatialRel","geometry");
parseSortables("orderBy");
}
*/
/**
* Parses and sets the maximum number of return records for the query filter.
* <br/>See: {@link DiscoveryFilter#setMaxRecords(int)}
* @param restKey the URL key for the parameter
* @param defaultValue the default value (if the parameter is not located on the URL)
*/
public void parseMaxRecords(String restKey, int defaultValue) {
getFilter().setMaxRecords(Val.chkInt(getRequestParameter(restKey),defaultValue));
}
/**
* Parses and appends a PropertyIsEqualTo clause to the query filter if located.
* <br/>See: {@link #extractProperty(PropertyClause, String, String)}
* <br/>See: {@link com.esri.gpt.catalog.discovery.PropertyClause.PropertyIsEqualTo}
* <br/>See: {@link RestQuery#getFilter}
* @param restKey the URL key for the parameter
* @param discoverableKey the key associated with the target discoverable
*/
public void parsePropertyIsEqualTo(String restKey, String discoverableKey) {
appendClause(getFilter().getRootClause(),
extractProperty(new PropertyClause.PropertyIsEqualTo(),restKey,discoverableKey));
}
/**
* Parses and appends a PropertyIsLike clause to the query filter if located.
* <br/>See: {@link #extractProperty(PropertyClause, String, String)}
* <br/>See: {@link com.esri.gpt.catalog.discovery.PropertyClause.PropertyIsLike}
* <br/>See: {@link RestQuery#getFilter}
* @param restKey the URL key for the parameter
* @param discoverableKey the key associated with the target discoverable
*/
public void parsePropertyIsLike(String restKey, String discoverableKey) {
appendClause(getFilter().getRootClause(),
extractProperty(new PropertyClause.PropertyIsLike(),restKey,discoverableKey));
}
/**
*
* Parses and appends a grouping of property clauses (delimited list) to the
* query filter if located.
* <br/>See: {@link RestQueryParser#extractPropertyList(String, String, String, boolean)}
* <br/>See: {@link RestQuery#getFilter}
* @param restKey the URL key for the parameter
* @param discoverableKey the key associated with the target discoverable
* @param delimiter the value delimiter
* @param orBased if true, group within a logical or, otherwise group within a logical and
*/
public void parsePropertyList(String restKey,
String discoverableKey,
String delimiter,
boolean orBased) {
appendClause(getFilter().getRootClause(),
extractPropertyList(restKey,discoverableKey,delimiter,orBased));
}
/**
* Parses and appends a range based discovery clause to the query filter if located.
* <br/>See: {@link #extractPropertyRange(String, String, String)}
* <br/>See: {@link RestQuery#getFilter}
* @param restLowerKey the URL key for the lower value parameter of the range
* @param restUpperKey the URL key for the upper value parameter of the range
* @param discoverableKey the key associated with the target discoverable
*/
public void parsePropertyRange(String restLowerKey, String restUpperKey, String discoverableKey) {
appendClause(getFilter().getRootClause(),
extractPropertyRange(restLowerKey,restUpperKey,discoverableKey));
}
/**
* Parses and sets the repository ID.
* <br/>See: {@link RestQuery#setRepositoryId(String)}
* @param restKey the URL key for the parameter
*/
public void parseRepositoryId(String restKey) {
getQuery().setRepositoryId(getRequestParameter(restKey));
}
/**
* Parses and sets the response format for the query.
* <br/>See: {@link RestQuery#setResponseFormat(String)}
* @param restKey the URL key for the parameter
*/
public void parseResponseFormat(String restKey) {
getQuery().setResponseFormat(getRequestParameter(restKey));
}
/**
* Parses and sets the response geometry for the query.
* <br/>See: {@link RestQuery#setResponseGeometry(String)}
* @param restKey the URL key for the parameter
*/
public void parseResponseGeometry(String restKey) {
getQuery().setResponseGeometry(getRequestParameter(restKey));
}
/**
* Parses and sets the response style for the query.
* <br/>See: {@link RestQuery#setResponseStyle(String)}
* @param restKey the URL key for the parameter
*/
public void parseResponseStyle(String restKey) {
getQuery().setResponseStyle(getRequestParameter(restKey));
}
/**
* Parses and sets the response target for the query.
* <br/>See: {@link RestQuery#setResponseTarget(String)}
* @param restKey the URL key for the parameter
*/
public void parseResponseTarget(String restKey) {
getQuery().setResponseTarget(getRequestParameter(restKey));
}
/**
* Parses sort option parameters and sets the query sortables if found.
* <br/>See: {@link RestQueryParser#extractSortables(String)}
* <br/>See: {@link RestQuery#setSortables(Sortables)}
* @param restKey the URL key for the parameter
*/
public void parseSortables(String restKey) {
getQuery().setSortables(extractSortables(restKey));
}
/**
* Parses and appends a spatial clause to the query filter if located.
* <br/>See: {@link #extractSpatialClause(String, String, String)}
* <br/>See: {@link RestQuery#getFilter}
* @param restBBoxKey the URL key for the BBOX parameter
* @param restOperatorKey the URL key for the spatial operator parameter
* @param discoverableKey the key associated with the target discoverable
*/
public void parseSpatialClause(String restBBoxKey,
String restOperatorKey,
String discoverableKey) {
appendClause(getFilter().getRootClause(),
extractSpatialClause(restBBoxKey,restOperatorKey,discoverableKey));
}
/**
* Parses and sets the start record for the query filter.
* <br/>See: {@link DiscoveryFilter#setStartRecord(int)}
* @param restKey the URL key for the parameter
* @param defaultValue the default value (if the parameter is not located on the URL)
*/
public void parseStartRecord(String restKey, int defaultValue) {
getFilter().setStartRecord(Val.chkInt(getRequestParameter(restKey),defaultValue));
}
/**
* Gets query string to more results.
* @param queryString initial query string
* @return query string to more results
*/
private String getMoreQueryString(String queryString) {
String [] props = queryString.split("&");
boolean found = false;
for (int i=0; i<props.length; i++) {
String prop = props[i];
if (prop.toLowerCase().startsWith("start=")) {
String [] kvp = prop.split("=");
if (kvp.length==2) {
int startRecord = Val.chkInt(kvp[1], 1);
props[i] = kvp[0] + "=" + Integer.toString(startRecord + getFilter().getMaxRecords());
found = true;
break;
}
}
}
StringBuilder sb = new StringBuilder();
for (String prop : props) {
if (sb.length()>0) sb.append("&");
sb.append(prop);
}
if (!found) {
if (sb.length()>0) sb.append("&");
sb.append("start=" + Integer.toString(getFilter().getStartRecord() + getFilter().getMaxRecords()));
}
return sb.toString();
}
}