/*
* Constellation - An open source and standard compliant SDI
* http://www.constellation-sdi.org
*
* Copyright 2014 Geomatys.
*
* Licensed 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 org.constellation.filter;
import org.apache.lucene.search.Filter;
import org.geotoolkit.lucene.filter.SerialChainFilter;
import org.geotoolkit.ogc.xml.v110.BinaryLogicOpType;
import org.geotoolkit.ogc.xml.v110.ComparisonOpsType;
import org.geotoolkit.ogc.xml.v110.FilterType;
import org.geotoolkit.ogc.xml.v110.LogicOpsType;
import org.geotoolkit.ogc.xml.v110.SpatialOpsType;
import org.geotoolkit.ogc.xml.v110.UnaryLogicOpType;
import org.geotoolkit.temporal.object.TemporalUtilities;
import org.opengis.filter.PropertyIsLike;
import org.opengis.filter.expression.PropertyName;
import javax.xml.bind.JAXBElement;
import javax.xml.namespace.QName;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import static org.geotoolkit.ows.xml.OWSExceptionCode.INVALID_PARAMETER_VALUE;
// JAXB dependencies
// Apache Lucene dependencies
// Geotoolkit dependencies
// GeoAPI dependencies
/**
* A parser for filter 1.1.0 and CQL 2.0
*
* @author Guilhem Legal
*/
public class SQLFilterParser extends FilterParser {
/**
* A map of variables (used in ebrim syntax).
*/
private Map<String, QName> variables;
/**
* A map of prefix and their correspounding namespace(used in ebrim syntax).
*/
private Map<String, String> prefixs;
private int nbField;
private boolean executeSelect;
private static final DateFormat DATE_FORMATTER;
static {
DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd' 'HH:mm:ss");
DATE_FORMATTER.setTimeZone(TimeZone.getTimeZone("GMT+0"));
}
/**
* {@inheritDoc}
*/
@Override
protected SQLQuery getNullFilter(final List<QName> typeNames) {
// TODO use typeNames
return new SQLQuery("Select \"identifier\" from \"Storage\".\"Records\" where \"recordSet\" != 'MDATA");
}
/**
* Build a lucene request from the specified Filter.
*
* @param filter a Filter object build directly from the XML or from a CQL request
*/
@Override
protected SQLQuery getQuery(final FilterType filter, Map<String, QName> variables, Map<String, String> prefixs, final List<QName> typeNames) throws FilterParserException {
this.variables = variables;
this.prefixs = prefixs;
executeSelect = true;
SQLQuery response = null;
if (filter != null) {
// we treat logical Operators like AND, OR, ...
if (filter.getLogicOps() != null) {
response = treatLogicalOperator(filter.getLogicOps());
// we treat directly comparison operator: PropertyIsLike, IsNull, IsBetween, ...
} else if (filter.getComparisonOps() != null) {
nbField = 1;
response = new SQLQuery(treatComparisonOperator(filter.getComparisonOps().getValue()));
// we treat spatial constraint : BBOX, Beyond, Overlaps, ...
} else if (filter.getSpatialOps() != null) {
response = new SQLQuery(treatSpatialOperator(filter.getSpatialOps()));
// we treat time operator: TimeAfter, TimeBefore, TimeDuring, ...
} else if (filter.getTemporalOps()!= null) {
response = new SQLQuery(treatTemporalOperator(filter.getTemporalOps().getValue()));
} else if (filter.getId() != null) {
response = new SQLQuery(treatIDOperator(filter.getId()));
}
}
if (response != null) {
response.nbField = nbField - 1;
if (executeSelect)
response.createSelect();
}
// TODO use typeNames
return response;
}
/**
* {@inheritDoc}
*/
@Override
protected SQLQuery treatLogicalOperator(final JAXBElement<? extends LogicOpsType> jbLogicOps) throws FilterParserException {
final List<SQLQuery> subQueries = new ArrayList<SQLQuery>();
final StringBuilder queryBuilder = new StringBuilder();
final LogicOpsType logicOps = jbLogicOps.getValue();
final String operator = jbLogicOps.getName().getLocalPart();
final List<Filter> filters = new ArrayList<Filter>();
nbField = 1;
if (logicOps instanceof BinaryLogicOpType) {
final BinaryLogicOpType binary = (BinaryLogicOpType) logicOps;
// we treat directly comparison operator: PropertyIsLike, IsNull, IsBetween, ...
for (JAXBElement<? extends ComparisonOpsType> jb: binary.getComparisonOps()) {
final SQLQuery query = new SQLQuery(treatComparisonOperator(jb.getValue()));
if (operator.equalsIgnoreCase("OR")) {
query.nbField = nbField -1;
query.createSelect();
queryBuilder.append('(').append(query.getQuery());
queryBuilder.append(") UNION ");
executeSelect = false;
} else {
queryBuilder.append(query.getQuery());
queryBuilder.append(" ").append(operator.toUpperCase()).append(" ");
}
}
// we treat logical Operators like AND, OR, ...
for (JAXBElement<? extends LogicOpsType> jb: binary.getLogicOps()) {
boolean writeOperator = true;
final SQLQuery query = treatLogicalOperator((JAXBElement<? extends LogicOpsType>)jb);
final String subQuery = query.getQuery();
final Filter subFilter = query.getSpatialFilter();
//if the sub spatial query contains both term search and spatial search we create a subQuery
if ((subFilter != null && !subQuery.isEmpty()) || !query.getSubQueries().isEmpty()) {
subQueries.add(query);
writeOperator = false;
} else {
if (subQuery.isEmpty()) {
writeOperator = false;
} else {
if (operator.equalsIgnoreCase("OR")) {
query.nbField = nbField -1;
query.createSelect();
queryBuilder.append('(').append(query.getQuery());
queryBuilder.append(") UNION ");
executeSelect = false;
} else {
queryBuilder.append(subQuery);
}
}
if (subFilter != null)
filters.add(subFilter);
}
if (writeOperator) {
queryBuilder.append(" ").append(operator.toUpperCase()).append(" ");
} else {
writeOperator = true;
}
}
// we treat spatial constraint : BBOX, Beyond, Overlaps, ...
for (JAXBElement<? extends SpatialOpsType> jb: binary.getSpatialOps()) {
boolean writeOperator = true;
//for the spatial filter we don't need to write into the lucene query
filters.add(treatSpatialOperator((JAXBElement<? extends SpatialOpsType>)jb));
writeOperator = false;
if (writeOperator) {
queryBuilder.append(" ").append(operator.toUpperCase()).append(" ");
} else {
writeOperator = true;
}
}
// we remove the last Operator and add a ') '
int pos;
if (operator.equalsIgnoreCase("OR"))
pos = queryBuilder.length()- 10;
else
pos = queryBuilder.length()- (operator.length() + 2);
if (pos > 0)
queryBuilder.delete(pos, queryBuilder.length());
} else if (logicOps instanceof UnaryLogicOpType) {
final UnaryLogicOpType unary = (UnaryLogicOpType) logicOps;
// we treat comparison operator: PropertyIsLike, IsNull, IsBetween, ...
if (unary.getComparisonOps() != null) {
queryBuilder.append(treatComparisonOperator(reverseComparisonOperator(unary.getComparisonOps().getValue())));
// we treat spatial constraint : BBOX, Beyond, Overlaps, ...
} else if (unary.getSpatialOps() != null) {
filters.add(treatSpatialOperator(unary.getSpatialOps()));
// we treat logical Operators like AND, OR, ...
} else if (unary.getLogicOps() != null) {
final SQLQuery sq = treatLogicalOperator(unary.getLogicOps());
final String subQuery = sq.getQuery();
final Filter subFilter = sq.getSpatialFilter();
/* if ((sq.getLogicalOperator() == SerialChainFilter.OR && subFilter != null && !subQuery.isEmpty()) ||
(sq.getLogicalOperator() == SerialChainFilter.NOT)) {
subQueries.add(sq);
} else {*/
if (!subQuery.isEmpty()) {
queryBuilder.append(subQuery);
}
if (subFilter != null) {
filters.add(sq.getSpatialFilter());
}
}
}
String query = queryBuilder.toString();
if ("()".equals(query)) {
query = "";
}
final int logicalOperand = SerialChainFilter.valueOf(operator);
final Filter spatialFilter = getSpatialFilterFromList(logicalOperand, filters);
final SQLQuery response = new SQLQuery(query, spatialFilter);
response.setSubQueries(subQueries);
return response;
}
/**
* {@inheritDoc}
*/
@Override
protected void addComparisonFilter(StringBuilder response, PropertyName propertyName, Object literalValue, String operator) throws FilterParserException {
response.append('v').append(nbField).append(".\"path\" = '").append(transformSyntax(propertyName.getPropertyName())).append("' AND ");
response.append('v').append(nbField).append(".\"value\" ").append(operator);
if (isDateField(propertyName)) {
literalValue = extractDateValue(literalValue);
}
if (literalValue != null) {
literalValue = literalValue.toString();
} else {
literalValue = "null";
}
if (!"IS NULL ".equals(operator)) {
response.append("'").append(literalValue).append("' ");
}
response.append(" AND v").append(nbField).append(".\"form\"=\"accessionNumber\" ");
nbField++;
}
/**
* {@inheritDoc}
*/
@Override
protected String extractDateValue(final Object literal) throws FilterParserException {
try {
synchronized (DATE_FORMATTER) {
final Date d;
if (literal instanceof Date) {
d = (Date)literal;
} else {
d = TemporalUtilities.parseDate(String.valueOf(literal));
}
return DATE_FORMATTER.format(d);
}
} catch (ParseException ex) {
throw new FilterParserException(PARSE_ERROR_MSG + literal, INVALID_PARAMETER_VALUE, QUERY_CONSTRAINT);
}
}
/**
* {@inheritDoc}
*/
@Override
protected String translateSpecialChar(final PropertyIsLike pil) {
return translateSpecialChar(pil, "%", "%", "\\");
}
/**
* Format the propertyName from ebrim syntax to mdweb syntax.
*/
private String transformSyntax(String s) {
if (s.indexOf(':') != -1) {
final String prefix = s.substring(0, s.lastIndexOf(':'));
s = s.replace(prefix, getStandardFromPrefix(prefix));
}
// we replace the variableName
for (String varName : variables.keySet()) {
final QName var = variables.get(varName);
final String mdwebVar = getStandardFromNamespace(var.getNamespaceURI()) + ':' + var.getLocalPart();
s = s.replace("$" + varName, mdwebVar);
}
// we replace the ebrim separator /@ by :
s = s.replace("/@", ":");
return s;
}
/**
* Return a MDweb standard name representation from a namespace URI.
*
* @param namespace
* @return
*/
private String getStandardFromNamespace(final String namespace) {
if ("http://www.opengis.net/cat/wrs/1.0".equals(namespace))
return "Web Registry Service v1.0";
else if ("http://www.opengis.net/cat/wrs".equals(namespace))
return "Web Registry Service v0.9";
else if ("urn:oasis:names:tc:ebxml-regrep:rim:xsd:2.5".equals(namespace))
return "Ebrim v2.5";
else if ("urn:oasis:names:tc:ebxml-regrep:xsd:rim:3.0".equals(namespace))
return "Ebrim v3.0";
else
throw new IllegalArgumentException("unexpected namespace: " + namespace);
}
/**
* Return a MDweb standard representation from a namespace URI or an abbreviated prefix.
* @param prefix
* @return
*/
private String getStandardFromPrefix(final String prefix) {
if (prefixs != null) {
final String namespace = prefixs.get(prefix);
if (namespace == null) {
return getStandardFromNamespace(prefix);
} else {
return getStandardFromNamespace(namespace);
}
}
return null;
}
}