mirror of https://github.com/apache/lucene.git
SOLR-8814: Support GeoJSON response format
This commit is contained in:
parent
cb85f2611e
commit
5731331be1
|
@ -31,6 +31,18 @@ Detailed Change List
|
||||||
New Features
|
New Features
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
* SOLR-8814: Support GeoJSON response writer and general spatial formatting. Adding
|
||||||
|
&wt=geojson&geojson.field=<your geometry field>
|
||||||
|
Will return a FeatureCollection for each SolrDocumentList and a Feature with the
|
||||||
|
requested geometry for each SolrDocument. The requested geometry field needs
|
||||||
|
to either extend AbstractSpatialFieldType or store a GeoJSON string. This also adds
|
||||||
|
a [geo] DocumentTransformer that can return the Shape in a variety of formats:
|
||||||
|
&fl=[geo f=<your geometry field> w=(GeoJSON|WKT|POLY)]
|
||||||
|
The default format is GeoJSON. For information on the supported formats, see:
|
||||||
|
https://github.com/locationtech/spatial4j/blob/master/FORMATS.md
|
||||||
|
To return the FeatureCollection as the root element, add '&omitHeader=true" (ryan)
|
||||||
|
|
||||||
|
|
||||||
Bug Fixes
|
Bug Fixes
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
|
|
@ -2123,10 +2123,11 @@ public final class SolrCore implements SolrInfoMBean, Closeable {
|
||||||
private final PluginBag<QueryResponseWriter> responseWriters = new PluginBag<>(QueryResponseWriter.class, this);
|
private final PluginBag<QueryResponseWriter> responseWriters = new PluginBag<>(QueryResponseWriter.class, this);
|
||||||
public static final Map<String ,QueryResponseWriter> DEFAULT_RESPONSE_WRITERS ;
|
public static final Map<String ,QueryResponseWriter> DEFAULT_RESPONSE_WRITERS ;
|
||||||
static{
|
static{
|
||||||
HashMap<String, QueryResponseWriter> m= new HashMap<>(14, 1);
|
HashMap<String, QueryResponseWriter> m= new HashMap<>(15, 1);
|
||||||
m.put("xml", new XMLResponseWriter());
|
m.put("xml", new XMLResponseWriter());
|
||||||
m.put("standard", m.get("xml"));
|
m.put("standard", m.get("xml"));
|
||||||
m.put(CommonParams.JSON, new JSONResponseWriter());
|
m.put(CommonParams.JSON, new JSONResponseWriter());
|
||||||
|
m.put("geojson", new GeoJSONResponseWriter());
|
||||||
m.put("python", new PythonResponseWriter());
|
m.put("python", new PythonResponseWriter());
|
||||||
m.put("php", new PHPResponseWriter());
|
m.put("php", new PHPResponseWriter());
|
||||||
m.put("phps", new PHPSerializedResponseWriter());
|
m.put("phps", new PHPSerializedResponseWriter());
|
||||||
|
|
|
@ -0,0 +1,345 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF 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 org.apache.solr.response;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.lucene.index.IndexableField;
|
||||||
|
import org.apache.solr.common.SolrDocument;
|
||||||
|
import org.apache.solr.common.SolrDocumentList;
|
||||||
|
import org.apache.solr.common.SolrException;
|
||||||
|
import org.apache.solr.common.SolrException.ErrorCode;
|
||||||
|
import org.apache.solr.common.params.CommonParams;
|
||||||
|
import org.apache.solr.common.util.NamedList;
|
||||||
|
import org.apache.solr.request.SolrQueryRequest;
|
||||||
|
import org.apache.solr.response.transform.GeoTransformerFactory;
|
||||||
|
import org.apache.solr.response.transform.WriteableGeoJSON;
|
||||||
|
import org.apache.solr.schema.AbstractSpatialFieldType;
|
||||||
|
import org.apache.solr.schema.SchemaField;
|
||||||
|
import org.apache.solr.search.ReturnFields;
|
||||||
|
import org.locationtech.spatial4j.context.SpatialContext;
|
||||||
|
import org.locationtech.spatial4j.io.ShapeWriter;
|
||||||
|
import org.locationtech.spatial4j.io.SupportedFormats;
|
||||||
|
import org.locationtech.spatial4j.shape.Shape;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend the standard JSONResponseWriter to support GeoJSON. This writes
|
||||||
|
* a {@link SolrDocumentList} with a 'FeatureCollection', following the
|
||||||
|
* specification in <a href="http://geojson.org/">geojson.org</a>
|
||||||
|
*/
|
||||||
|
public class GeoJSONResponseWriter extends JSONResponseWriter {
|
||||||
|
|
||||||
|
public static final String FIELD = "geojson.field";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
|
||||||
|
|
||||||
|
String geofield = req.getParams().get(FIELD, null);
|
||||||
|
if(geofield==null || geofield.length()==0) {
|
||||||
|
throw new SolrException(ErrorCode.BAD_REQUEST, "GeoJSON. Missing parameter: '"+FIELD+"'");
|
||||||
|
}
|
||||||
|
|
||||||
|
SchemaField sf = req.getSchema().getFieldOrNull(geofield);
|
||||||
|
if(sf==null) {
|
||||||
|
throw new SolrException(ErrorCode.BAD_REQUEST, "GeoJSON. Unknown field: '"+FIELD+"'="+geofield);
|
||||||
|
}
|
||||||
|
|
||||||
|
SupportedFormats formats = null;
|
||||||
|
if(sf.getType() instanceof AbstractSpatialFieldType) {
|
||||||
|
SpatialContext ctx = ((AbstractSpatialFieldType)sf.getType()).getSpatialContext();
|
||||||
|
formats = ctx.getFormats();
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONWriter w = new GeoJSONWriter(writer, req, rsp,
|
||||||
|
geofield,
|
||||||
|
formats);
|
||||||
|
|
||||||
|
try {
|
||||||
|
w.writeResponse();
|
||||||
|
} finally {
|
||||||
|
w.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeoJSONWriter extends JSONWriter {
|
||||||
|
|
||||||
|
final SupportedFormats formats;
|
||||||
|
final ShapeWriter geowriter;
|
||||||
|
final String geofield;
|
||||||
|
|
||||||
|
public GeoJSONWriter(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp,
|
||||||
|
String geofield, SupportedFormats formats) {
|
||||||
|
super(writer, req, rsp);
|
||||||
|
this.geofield = geofield;
|
||||||
|
this.formats = formats;
|
||||||
|
if(formats==null) {
|
||||||
|
this.geowriter = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.geowriter = formats.getGeoJsonWriter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeResponse() throws IOException {
|
||||||
|
if(req.getParams().getBool(CommonParams.OMIT_HEADER, false)) {
|
||||||
|
if(wrapperFunction!=null) {
|
||||||
|
writer.write(wrapperFunction + "(");
|
||||||
|
}
|
||||||
|
rsp.removeResponseHeader();
|
||||||
|
|
||||||
|
NamedList<Object> vals = rsp.getValues();
|
||||||
|
Object response = vals.remove("response");
|
||||||
|
if(vals.size()==0) {
|
||||||
|
writeVal(null, response);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new SolrException(ErrorCode.BAD_REQUEST,
|
||||||
|
"GeoJSON with "+CommonParams.OMIT_HEADER +
|
||||||
|
" can not return more than a result set");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(wrapperFunction!=null) {
|
||||||
|
writer.write(')');
|
||||||
|
}
|
||||||
|
writer.write('\n'); // ending with a newline looks much better from the command line
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
super.writeResponse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeSolrDocument(String name, SolrDocument doc, ReturnFields returnFields, int idx) throws IOException {
|
||||||
|
if( idx > 0 ) {
|
||||||
|
writeArraySeparator();
|
||||||
|
}
|
||||||
|
|
||||||
|
indent();
|
||||||
|
writeMapOpener(-1);
|
||||||
|
incLevel();
|
||||||
|
|
||||||
|
writeKey("type", false);
|
||||||
|
writeVal(null, "Feature");
|
||||||
|
|
||||||
|
Object val = doc.getFieldValue(geofield);
|
||||||
|
if(val != null) {
|
||||||
|
writeFeatureGeometry(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean first=true;
|
||||||
|
for (String fname : doc.getFieldNames()) {
|
||||||
|
if (fname.equals(geofield) || ((returnFields!= null && !returnFields.wantsField(fname)))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
writeMapSeparator();
|
||||||
|
if (first) {
|
||||||
|
indent();
|
||||||
|
writeKey("properties", false);
|
||||||
|
writeMapOpener(-1);
|
||||||
|
incLevel();
|
||||||
|
|
||||||
|
first=false;
|
||||||
|
}
|
||||||
|
|
||||||
|
indent();
|
||||||
|
writeKey(fname, true);
|
||||||
|
val = doc.getFieldValue(fname);
|
||||||
|
|
||||||
|
// SolrDocument will now have multiValued fields represented as a Collection,
|
||||||
|
// even if only a single value is returned for this document.
|
||||||
|
if (val instanceof List) {
|
||||||
|
// shortcut this common case instead of going through writeVal again
|
||||||
|
writeArray(name,((Iterable)val).iterator());
|
||||||
|
} else {
|
||||||
|
writeVal(fname, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeoJSON does not really support nested FeatureCollections
|
||||||
|
if(doc.hasChildDocuments()) {
|
||||||
|
if(first == false) {
|
||||||
|
writeMapSeparator();
|
||||||
|
indent();
|
||||||
|
}
|
||||||
|
writeKey("_childDocuments_", true);
|
||||||
|
writeArrayOpener(doc.getChildDocumentCount());
|
||||||
|
List<SolrDocument> childDocs = doc.getChildDocuments();
|
||||||
|
for(int i=0; i<childDocs.size(); i++) {
|
||||||
|
writeSolrDocument(null, childDocs.get(i), null, i);
|
||||||
|
}
|
||||||
|
writeArrayCloser();
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that we added any properties
|
||||||
|
if(!first) {
|
||||||
|
decLevel();
|
||||||
|
writeMapCloser();
|
||||||
|
}
|
||||||
|
|
||||||
|
decLevel();
|
||||||
|
writeMapCloser();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void writeFeatureGeometry(Object geo) throws IOException
|
||||||
|
{
|
||||||
|
// Support multi-valued geometries
|
||||||
|
if(geo instanceof Iterable) {
|
||||||
|
Iterator iter = ((Iterable)geo).iterator();
|
||||||
|
if(!iter.hasNext()) {
|
||||||
|
return; // empty list
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
geo = iter.next();
|
||||||
|
|
||||||
|
// More than value
|
||||||
|
if(iter.hasNext()) {
|
||||||
|
writeMapSeparator();
|
||||||
|
indent();
|
||||||
|
writeKey("geometry", false);
|
||||||
|
incLevel();
|
||||||
|
|
||||||
|
// TODO: in the future, we can be smart and try to make this the appropriate MULTI* value
|
||||||
|
// if all the values are the same
|
||||||
|
// { "type": "GeometryCollection",
|
||||||
|
// "geometries": [
|
||||||
|
writeMapOpener(-1);
|
||||||
|
writeKey("type",false);
|
||||||
|
writeStr(null, "GeometryCollection", false);
|
||||||
|
writeMapSeparator();
|
||||||
|
writeKey("geometries", false);
|
||||||
|
writeArrayOpener(-1); // no trivial way to determine array size
|
||||||
|
incLevel();
|
||||||
|
|
||||||
|
// The first one
|
||||||
|
indent();
|
||||||
|
writeGeo(geo);
|
||||||
|
while(iter.hasNext()) {
|
||||||
|
// Each element in the array
|
||||||
|
writeArraySeparator();
|
||||||
|
indent();
|
||||||
|
writeGeo(iter.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
decLevel();
|
||||||
|
writeArrayCloser();
|
||||||
|
writeMapCloser();
|
||||||
|
|
||||||
|
decLevel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single Value
|
||||||
|
if(geo!=null) {
|
||||||
|
writeMapSeparator();
|
||||||
|
indent();
|
||||||
|
writeKey("geometry", false);
|
||||||
|
writeGeo(geo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void writeGeo(Object geo) throws IOException {
|
||||||
|
Shape shape = null;
|
||||||
|
String str = null;
|
||||||
|
if(geo instanceof Shape) {
|
||||||
|
shape = (Shape)geo;
|
||||||
|
}
|
||||||
|
else if(geo instanceof IndexableField) {
|
||||||
|
str = ((IndexableField)geo).stringValue();
|
||||||
|
}
|
||||||
|
else if(geo instanceof WriteableGeoJSON) {
|
||||||
|
shape = ((WriteableGeoJSON)geo).shape;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
str = geo.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(str !=null) {
|
||||||
|
// Assume it is well formed JSON
|
||||||
|
if(str.startsWith("{") && str.endsWith("}")) {
|
||||||
|
writer.write(str);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(formats==null) {
|
||||||
|
// The check is here and not in the constructor because we do not know if the
|
||||||
|
// *stored* values for the field look like JSON until we actually try to read them
|
||||||
|
throw new SolrException(ErrorCode.BAD_REQUEST,
|
||||||
|
"GeoJSON unable to write field: '&"+ GeoJSONResponseWriter.FIELD +"="+geofield+"' ("+str+")");
|
||||||
|
}
|
||||||
|
shape = formats.read(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(geowriter==null) {
|
||||||
|
throw new SolrException(ErrorCode.BAD_REQUEST,
|
||||||
|
"GeoJSON unable to write field: '&"+ GeoJSONResponseWriter.FIELD +"="+geofield+"'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(shape!=null) {
|
||||||
|
geowriter.write(writer, shape);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeStartDocumentList(String name,
|
||||||
|
long start, int size, long numFound, Float maxScore) throws IOException
|
||||||
|
{
|
||||||
|
writeMapOpener((maxScore==null) ? 3 : 4);
|
||||||
|
incLevel();
|
||||||
|
writeKey("type",false);
|
||||||
|
writeStr(null, "FeatureCollection", false);
|
||||||
|
writeMapSeparator();
|
||||||
|
writeKey("numFound",false);
|
||||||
|
writeLong(null,numFound);
|
||||||
|
writeMapSeparator();
|
||||||
|
writeKey("start",false);
|
||||||
|
writeLong(null,start);
|
||||||
|
|
||||||
|
if (maxScore!=null) {
|
||||||
|
writeMapSeparator();
|
||||||
|
writeKey("maxScore",false);
|
||||||
|
writeFloat(null,maxScore);
|
||||||
|
}
|
||||||
|
writeMapSeparator();
|
||||||
|
|
||||||
|
// if can we get bbox of all results, we should write it here
|
||||||
|
|
||||||
|
// indent();
|
||||||
|
writeKey("features",false);
|
||||||
|
writeArrayOpener(size);
|
||||||
|
|
||||||
|
incLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeEndDocumentList() throws IOException
|
||||||
|
{
|
||||||
|
decLevel();
|
||||||
|
writeArrayCloser();
|
||||||
|
|
||||||
|
decLevel();
|
||||||
|
indent();
|
||||||
|
writeMapCloser();
|
||||||
|
}
|
||||||
|
}
|
|
@ -70,8 +70,8 @@ public class JSONResponseWriter implements QueryResponseWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
class JSONWriter extends TextResponseWriter {
|
class JSONWriter extends TextResponseWriter {
|
||||||
|
protected String wrapperFunction;
|
||||||
private String namedListStyle;
|
private String namedListStyle;
|
||||||
private String wrapperFunction;
|
|
||||||
|
|
||||||
private static final String JSON_NL_STYLE="json.nl";
|
private static final String JSON_NL_STYLE="json.nl";
|
||||||
private static final String JSON_NL_MAP="map";
|
private static final String JSON_NL_MAP="map";
|
||||||
|
|
|
@ -0,0 +1,224 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF 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 org.apache.solr.response.transform;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
import org.apache.lucene.index.IndexableField;
|
||||||
|
import org.apache.lucene.queries.function.ValueSource;
|
||||||
|
import org.apache.lucene.search.MatchAllDocsQuery;
|
||||||
|
import org.apache.lucene.search.Query;
|
||||||
|
import org.apache.lucene.spatial.SpatialStrategy;
|
||||||
|
import org.apache.lucene.spatial.composite.CompositeSpatialStrategy;
|
||||||
|
import org.apache.lucene.spatial.serialized.SerializedDVStrategy;
|
||||||
|
import org.apache.solr.common.SolrDocument;
|
||||||
|
import org.apache.solr.common.SolrException;
|
||||||
|
import org.apache.solr.common.SolrException.ErrorCode;
|
||||||
|
import org.apache.solr.common.params.SolrParams;
|
||||||
|
import org.apache.solr.request.SolrQueryRequest;
|
||||||
|
import org.apache.solr.response.JSONResponseWriter;
|
||||||
|
import org.apache.solr.response.QueryResponseWriter;
|
||||||
|
import org.apache.solr.schema.AbstractSpatialFieldType;
|
||||||
|
import org.apache.solr.schema.SchemaField;
|
||||||
|
import org.apache.solr.search.QParser;
|
||||||
|
import org.apache.solr.search.SyntaxError;
|
||||||
|
import org.locationtech.spatial4j.io.GeoJSONWriter;
|
||||||
|
import org.locationtech.spatial4j.io.ShapeWriter;
|
||||||
|
import org.locationtech.spatial4j.io.SupportedFormats;
|
||||||
|
import org.locationtech.spatial4j.shape.Shape;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This DocumentTransformer will write a {@link Shape} to the SolrDocument using
|
||||||
|
* the requested format. Supported formats include:
|
||||||
|
* <ul>
|
||||||
|
* <li>GeoJSON</li>
|
||||||
|
* <li>WKT</li>
|
||||||
|
* <li>Polyshape</li>
|
||||||
|
* </ul>
|
||||||
|
* For more information see: <a href="https://github.com/locationtech/spatial4j/blob/master/FORMATS.md">spatial4j/FORMATS.md</a>
|
||||||
|
*
|
||||||
|
* The shape is either read from a stored field, or a ValueSource.
|
||||||
|
*
|
||||||
|
* This transformer is useful when:
|
||||||
|
* <ul>
|
||||||
|
* <li>You want to return a format different than the stored encoding (WKT vs GeoJSON)</li>
|
||||||
|
* <li>The {@link Shape} is stored in a {@link ValueSource}, not a stored field</li>
|
||||||
|
* <li>the value is not stored in a format the output understands (ie, raw GeoJSON)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class GeoTransformerFactory extends TransformerFactory
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public DocTransformer create(String display, SolrParams params, SolrQueryRequest req) {
|
||||||
|
|
||||||
|
String fname = params.get("f", display);
|
||||||
|
if(fname.startsWith("[") && fname.endsWith("]")) {
|
||||||
|
fname = display.substring(1,display.length()-1);
|
||||||
|
}
|
||||||
|
SchemaField sf = req.getSchema().getFieldOrNull(fname);
|
||||||
|
if(sf==null) {
|
||||||
|
throw new SolrException(ErrorCode.BAD_REQUEST,
|
||||||
|
this.getClass().getSimpleName() +" using unknown field: "+fname);
|
||||||
|
}
|
||||||
|
if(!(sf.getType() instanceof AbstractSpatialFieldType)) {
|
||||||
|
throw new SolrException(ErrorCode.BAD_REQUEST,
|
||||||
|
"GeoTransformer requested non-spatial field: "+fname + " ("+sf.getType().getClass().getSimpleName()+")");
|
||||||
|
}
|
||||||
|
|
||||||
|
final GeoFieldUpdater updater = new GeoFieldUpdater();
|
||||||
|
updater.field = fname;
|
||||||
|
updater.display = display;
|
||||||
|
updater.display_error = display+"_error";
|
||||||
|
|
||||||
|
ValueSource shapes = null;
|
||||||
|
AbstractSpatialFieldType<?> sdv = (AbstractSpatialFieldType<?>)sf.getType();
|
||||||
|
SpatialStrategy strategy = sdv.getStrategy(fname);
|
||||||
|
if(strategy instanceof CompositeSpatialStrategy) {
|
||||||
|
shapes = ((CompositeSpatialStrategy)strategy)
|
||||||
|
.getGeometryStrategy().makeShapeValueSource();
|
||||||
|
}
|
||||||
|
else if(strategy instanceof SerializedDVStrategy) {
|
||||||
|
shapes = ((SerializedDVStrategy)strategy)
|
||||||
|
.makeShapeValueSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String writerName = params.get("w", "GeoJSON");
|
||||||
|
updater.formats = strategy.getSpatialContext().getFormats();
|
||||||
|
updater.writer = updater.formats.getWriter(writerName);
|
||||||
|
if(updater.writer==null) {
|
||||||
|
StringBuilder str = new StringBuilder();
|
||||||
|
str.append( "Unknown Spatial Writer: " ).append(writerName);
|
||||||
|
str.append(" [");
|
||||||
|
for(ShapeWriter w : updater.formats.getWriters()) {
|
||||||
|
str.append(w.getFormatName()).append(' ');
|
||||||
|
}
|
||||||
|
str.append("]");
|
||||||
|
throw new SolrException(ErrorCode.BAD_REQUEST, str.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResponseWriter qw = req.getCore().getQueryResponseWriter(req);
|
||||||
|
updater.isJSON =
|
||||||
|
(qw.getClass() == JSONResponseWriter.class) &&
|
||||||
|
(updater.writer instanceof GeoJSONWriter);
|
||||||
|
|
||||||
|
|
||||||
|
// Using ValueSource
|
||||||
|
if(shapes!=null) {
|
||||||
|
// we don't really need the qparser... just so we can reuse valueSource
|
||||||
|
QParser parser = new QParser(null,null,params, req) {
|
||||||
|
@Override
|
||||||
|
public Query parse() throws SyntaxError {
|
||||||
|
return new MatchAllDocsQuery();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new ValueSourceAugmenter(display, parser, shapes) {
|
||||||
|
@Override
|
||||||
|
protected void setValue(SolrDocument doc, Object val) {
|
||||||
|
updater.setValue(doc, val);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using the raw stored values
|
||||||
|
return new DocTransformer() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void transform(SolrDocument doc, int docid, float score) throws IOException {
|
||||||
|
Object val = doc.remove(updater.field);
|
||||||
|
if(val!=null) {
|
||||||
|
updater.setValue(doc, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return updater.display;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getExtraRequestFields() {
|
||||||
|
return new String[] {updater.field};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeoFieldUpdater {
|
||||||
|
String field;
|
||||||
|
String display;
|
||||||
|
String display_error;
|
||||||
|
|
||||||
|
boolean isJSON;
|
||||||
|
ShapeWriter writer;
|
||||||
|
SupportedFormats formats;
|
||||||
|
|
||||||
|
void addShape(SolrDocument doc, Shape shape) {
|
||||||
|
if(isJSON) {
|
||||||
|
doc.addField(display, new WriteableGeoJSON(shape, writer));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
doc.addField(display, writer.toString(shape));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setValue(SolrDocument doc, Object val) {
|
||||||
|
doc.remove(display);
|
||||||
|
if(val != null) {
|
||||||
|
if(val instanceof Iterable) {
|
||||||
|
Iterator iter = ((Iterable)val).iterator();
|
||||||
|
while(iter.hasNext()) {
|
||||||
|
addValue(doc, iter.next());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
addValue(doc, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addValue(SolrDocument doc, Object val) {
|
||||||
|
if(val == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(val instanceof Shape) {
|
||||||
|
addShape(doc, (Shape)val);
|
||||||
|
}
|
||||||
|
// Don't explode on 'InvalidShpae'
|
||||||
|
else if( val instanceof Exception) {
|
||||||
|
doc.setField( display_error, ((Exception)val).toString() );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Use the stored value
|
||||||
|
if(val instanceof IndexableField) {
|
||||||
|
val = ((IndexableField)val).stringValue();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
addShape(doc, formats.read(val.toString()));
|
||||||
|
}
|
||||||
|
catch(Exception ex) {
|
||||||
|
doc.setField( display_error, ex.toString() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -49,5 +49,6 @@ public abstract class TransformerFactory implements NamedListInitializedPlugin
|
||||||
defaultFactories.put( "child", new ChildDocTransformerFactory() );
|
defaultFactories.put( "child", new ChildDocTransformerFactory() );
|
||||||
defaultFactories.put( "json", new RawValueTransformerFactory("json") );
|
defaultFactories.put( "json", new RawValueTransformerFactory("json") );
|
||||||
defaultFactories.put( "xml", new RawValueTransformerFactory("xml") );
|
defaultFactories.put( "xml", new RawValueTransformerFactory("xml") );
|
||||||
|
defaultFactories.put( "geo", new GeoTransformerFactory() );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF 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 org.apache.solr.response.transform;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.solr.common.util.JavaBinCodec;
|
||||||
|
import org.apache.solr.response.TextResponseWriter;
|
||||||
|
import org.apache.solr.response.WriteableValue;
|
||||||
|
import org.locationtech.spatial4j.io.ShapeWriter;
|
||||||
|
import org.locationtech.spatial4j.shape.Shape;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will let the writer add values to the response directly
|
||||||
|
*/
|
||||||
|
public class WriteableGeoJSON extends WriteableValue {
|
||||||
|
|
||||||
|
public final Shape shape;
|
||||||
|
public final ShapeWriter jsonWriter;
|
||||||
|
|
||||||
|
public WriteableGeoJSON(Shape shape, ShapeWriter jsonWriter) {
|
||||||
|
this.shape = shape;
|
||||||
|
this.jsonWriter = jsonWriter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object resolve(Object o, JavaBinCodec codec) throws IOException {
|
||||||
|
codec.writeStr(jsonWriter.toString(shape));
|
||||||
|
return null; // this means we wrote it
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(String name, TextResponseWriter writer) throws IOException {
|
||||||
|
jsonWriter.write(writer.getWriter(), shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return jsonWriter.toString(shape);
|
||||||
|
}
|
||||||
|
}
|
|
@ -390,6 +390,13 @@ public abstract class AbstractSpatialFieldType<T extends SpatialStrategy> extend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The Spatial Context for this field type
|
||||||
|
*/
|
||||||
|
public SpatialContext getSpatialContext() {
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(TextResponseWriter writer, String name, IndexableField f) throws IOException {
|
public void write(TextResponseWriter writer, String name, IndexableField f) throws IOException {
|
||||||
writer.writeStr(name, f.stringValue(), true);
|
writer.writeStr(name, f.stringValue(), true);
|
||||||
|
|
|
@ -70,6 +70,7 @@
|
||||||
<field name="bbox" type="bbox" />
|
<field name="bbox" type="bbox" />
|
||||||
|
|
||||||
<dynamicField name="bboxD_*" type="bbox" indexed="true" />
|
<dynamicField name="bboxD_*" type="bbox" indexed="true" />
|
||||||
|
<dynamicField name="str_*" type="string" indexed="true" stored="true"/>
|
||||||
|
|
||||||
</fields>
|
</fields>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,279 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF 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 org.apache.solr.response;
|
||||||
|
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.solr.SolrTestCaseJ4;
|
||||||
|
import org.apache.solr.common.SolrException;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.locationtech.spatial4j.context.SpatialContext;
|
||||||
|
import org.locationtech.spatial4j.io.SupportedFormats;
|
||||||
|
import org.locationtech.spatial4j.shape.Shape;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
public class TestGeoJSONResponseWriter extends SolrTestCaseJ4 {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
|
||||||
|
|
||||||
|
final ObjectMapper jsonmapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void beforeClass() throws Exception {
|
||||||
|
initCore("solrconfig-basic.xml","schema-spatial.xml");
|
||||||
|
createIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void createIndex() {
|
||||||
|
|
||||||
|
|
||||||
|
// <field name="srpt_geohash" type="srpt_geohash" multiValued="true" />
|
||||||
|
// <field name="" type="srpt_quad" multiValued="true" />
|
||||||
|
// <field name="" type="srpt_packedquad" multiValued="true" />
|
||||||
|
// <field name="" type="stqpt_geohash" multiValued="true" />
|
||||||
|
|
||||||
|
// multiple valued field
|
||||||
|
assertU(adoc("id","H.A", "srpt_geohash","POINT( 1 2 )"));
|
||||||
|
assertU(adoc("id","H.B", "srpt_geohash","POINT( 1 2 )",
|
||||||
|
"srpt_geohash","POINT( 3 4 )"));
|
||||||
|
assertU(adoc("id","H.C", "srpt_geohash","LINESTRING (30 10, 10 30, 40 40)"));
|
||||||
|
|
||||||
|
assertU(adoc("id","Q.A", "srpt_quad","POINT( 1 2 )"));
|
||||||
|
assertU(adoc("id","Q.B", "srpt_quad","POINT( 1 2 )",
|
||||||
|
"srpt_quad","POINT( 3 4 )"));
|
||||||
|
assertU(adoc("id","Q.C", "srpt_quad","LINESTRING (30 10, 10 30, 40 40)"));
|
||||||
|
|
||||||
|
assertU(adoc("id","P.A", "srpt_packedquad","POINT( 1 2 )"));
|
||||||
|
assertU(adoc("id","P.B", "srpt_packedquad","POINT( 1 2 )",
|
||||||
|
"srpt_packedquad","POINT( 3 4 )"));
|
||||||
|
assertU(adoc("id","P.C", "srpt_packedquad","LINESTRING (30 10, 10 30, 40 40)"));
|
||||||
|
|
||||||
|
|
||||||
|
// single valued field
|
||||||
|
assertU(adoc("id","R.A", "srptgeom","POINT( 1 2 )"));
|
||||||
|
|
||||||
|
// non-spatial field
|
||||||
|
assertU(adoc("id","S.X", "str_shape","POINT( 1 2 )"));
|
||||||
|
assertU(adoc("id","S.A", "str_shape","{\"type\":\"Point\",\"coordinates\":[1,2]}"));
|
||||||
|
|
||||||
|
|
||||||
|
assertU(commit());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Map<String,Object> readJSON(String json) {
|
||||||
|
try {
|
||||||
|
return jsonmapper.readValue(json, Map.class);
|
||||||
|
}
|
||||||
|
catch(Exception ex) {
|
||||||
|
log.warn("Unable to read GeoJSON From: {}", json);
|
||||||
|
log.warn("Error", ex);
|
||||||
|
fail("Unable to parse JSON GeoJSON Response");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Map<String,Object> getFirstFeatureGeometry(Map<String,Object> json)
|
||||||
|
{
|
||||||
|
Map<String,Object> rsp = (Map<String,Object>)json.get("response");
|
||||||
|
assertEquals("FeatureCollection", rsp.get("type"));
|
||||||
|
List<Object> vals = (List<Object>)rsp.get("features");
|
||||||
|
assertEquals(1, vals.size());
|
||||||
|
Map<String,Object> feature = (Map<String,Object>)vals.get(0);
|
||||||
|
assertEquals("Feature", feature.get("type"));
|
||||||
|
return (Map<String,Object>)feature.get("geometry");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRequestExceptions() throws Exception {
|
||||||
|
|
||||||
|
// Make sure we select the field
|
||||||
|
try {
|
||||||
|
h.query(req(
|
||||||
|
"q","*:*",
|
||||||
|
"wt","geojson",
|
||||||
|
"fl","*"));
|
||||||
|
fail("should Require a parameter to select the field");
|
||||||
|
}
|
||||||
|
catch(SolrException ex) {}
|
||||||
|
|
||||||
|
|
||||||
|
// non-spatial fields *must* be stored as JSON
|
||||||
|
try {
|
||||||
|
h.query(req(
|
||||||
|
"q","id:S.X",
|
||||||
|
"wt","geojson",
|
||||||
|
"fl","*",
|
||||||
|
"geojson.field", "str_shape"));
|
||||||
|
fail("should complain about bad shape config");
|
||||||
|
}
|
||||||
|
catch(SolrException ex) {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGeoJSONAtRoot() throws Exception {
|
||||||
|
|
||||||
|
// Try reading the whole resposne
|
||||||
|
String json = h.query(req(
|
||||||
|
"q","*:*",
|
||||||
|
"wt","geojson",
|
||||||
|
"rows","2",
|
||||||
|
"fl","*",
|
||||||
|
"geojson.field", "stqpt_geohash",
|
||||||
|
"indent","true"));
|
||||||
|
|
||||||
|
// Check that we have a normal solr response with 'responseHeader' and 'response'
|
||||||
|
Map<String,Object> rsp = readJSON(json);
|
||||||
|
assertNotNull(rsp.get("responseHeader"));
|
||||||
|
assertNotNull(rsp.get("response"));
|
||||||
|
|
||||||
|
json = h.query(req(
|
||||||
|
"q","*:*",
|
||||||
|
"wt","geojson",
|
||||||
|
"rows","2",
|
||||||
|
"fl","*",
|
||||||
|
"omitHeader", "true",
|
||||||
|
"geojson.field", "stqpt_geohash",
|
||||||
|
"indent","true"));
|
||||||
|
|
||||||
|
// Check that we have a normal solr response with 'responseHeader' and 'response'
|
||||||
|
rsp = readJSON(json);
|
||||||
|
assertNull(rsp.get("responseHeader"));
|
||||||
|
assertNull(rsp.get("response"));
|
||||||
|
assertEquals("FeatureCollection", rsp.get("type"));
|
||||||
|
assertNotNull(rsp.get("features"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGeoJSONOutput() throws Exception {
|
||||||
|
|
||||||
|
// Try reading the whole resposne
|
||||||
|
readJSON(h.query(req(
|
||||||
|
"q","*:*",
|
||||||
|
"wt","geojson",
|
||||||
|
"fl","*",
|
||||||
|
"geojson.field", "stqpt_geohash",
|
||||||
|
"indent","true")));
|
||||||
|
|
||||||
|
// Multivalued Valued Point
|
||||||
|
Map<String,Object> json = readJSON(h.query(req(
|
||||||
|
"q","id:H.B",
|
||||||
|
"wt","geojson",
|
||||||
|
"fl","*",
|
||||||
|
"geojson.field", "srpt_geohash",
|
||||||
|
"indent","true")));
|
||||||
|
|
||||||
|
Map<String,Object> geo = getFirstFeatureGeometry(json);
|
||||||
|
assertEquals( // NOTE: not actual JSON, it is Map.toString()!
|
||||||
|
"{type=GeometryCollection, geometries=["
|
||||||
|
+ "{type=Point, coordinates=[1, 2]}, "
|
||||||
|
+ "{type=Point, coordinates=[3, 4]}]}", ""+geo);
|
||||||
|
|
||||||
|
|
||||||
|
// Check the same value encoded on different field types
|
||||||
|
String[][] check = new String[][] {
|
||||||
|
{ "id:H.A", "srpt_geohash" },
|
||||||
|
{ "id:Q.A", "srpt_quad" },
|
||||||
|
{ "id:P.A", "srpt_packedquad" },
|
||||||
|
{ "id:R.A", "srptgeom" },
|
||||||
|
{ "id:S.A", "str_shape" },
|
||||||
|
};
|
||||||
|
|
||||||
|
for(String[] args : check) {
|
||||||
|
json = readJSON(h.query(req(
|
||||||
|
"q",args[0],
|
||||||
|
"wt","geojson",
|
||||||
|
"fl","*",
|
||||||
|
"geojson.field", args[1])));
|
||||||
|
|
||||||
|
geo = getFirstFeatureGeometry(json);
|
||||||
|
assertEquals(
|
||||||
|
"Error reading point from: "+args[1] + " ("+args[0]+")",
|
||||||
|
// NOTE: not actual JSON, it is Map.toString()!
|
||||||
|
"{type=Point, coordinates=[1, 2]}", ""+geo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Map<String,Object> readFirstDoc(String json)
|
||||||
|
{
|
||||||
|
List docs = (List)((Map)readJSON(json).get("response")).get("docs");
|
||||||
|
return (Map)docs.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String normalizeMapToJSON(String val) {
|
||||||
|
val = val.replace("\"", ""); // remove quotes
|
||||||
|
val = val.replace(':', '=');
|
||||||
|
val = val.replace(", ", ",");
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformToAllFormats() throws Exception {
|
||||||
|
|
||||||
|
String wkt = "POINT( 1 2 )";
|
||||||
|
SupportedFormats fmts = SpatialContext.GEO.getFormats();
|
||||||
|
Shape shape = fmts.read(wkt);
|
||||||
|
|
||||||
|
String[] check = new String[] {
|
||||||
|
"srpt_geohash",
|
||||||
|
"srpt_geohash",
|
||||||
|
"srpt_quad",
|
||||||
|
"srpt_packedquad",
|
||||||
|
"srptgeom",
|
||||||
|
// "str_shape", // NEEDS TO BE A SpatialField!
|
||||||
|
};
|
||||||
|
|
||||||
|
String[] checkFormats = new String[] {
|
||||||
|
"GeoJSON",
|
||||||
|
"WKT",
|
||||||
|
"POLY"
|
||||||
|
};
|
||||||
|
|
||||||
|
for(String field : check) {
|
||||||
|
// Add a document with the given field
|
||||||
|
assertU(adoc("id","test",
|
||||||
|
field, wkt));
|
||||||
|
assertU(commit());
|
||||||
|
|
||||||
|
|
||||||
|
for(String fmt : checkFormats) {
|
||||||
|
String json = h.query(req(
|
||||||
|
"q","id:test",
|
||||||
|
"wt","json",
|
||||||
|
"indent", "true",
|
||||||
|
"fl","xxx:[geo f="+field+" w="+fmt+"]"
|
||||||
|
));
|
||||||
|
|
||||||
|
Map<String,Object> doc = readFirstDoc(json);
|
||||||
|
Object v = doc.get("xxx");
|
||||||
|
String expect = fmts.getWriter(fmt).toString(shape);
|
||||||
|
|
||||||
|
if(!(v instanceof String)) {
|
||||||
|
v = normalizeMapToJSON(v.toString());
|
||||||
|
expect = normalizeMapToJSON(expect);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("Bad result: "+field+"/"+fmt, expect, v.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue