mirror of https://github.com/apache/lucene.git
SOLR-8814: Support GeoJSON response format
This commit is contained in:
parent
24830b7f18
commit
36145d02cc
|
@ -34,6 +34,18 @@ Detailed Change List
|
|||
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
|
||||
----------------------
|
||||
|
||||
|
|
|
@ -2123,10 +2123,11 @@ public final class SolrCore implements SolrInfoMBean, Closeable {
|
|||
private final PluginBag<QueryResponseWriter> responseWriters = new PluginBag<>(QueryResponseWriter.class, this);
|
||||
public static final Map<String ,QueryResponseWriter> DEFAULT_RESPONSE_WRITERS ;
|
||||
static{
|
||||
HashMap<String, QueryResponseWriter> m= new HashMap<>(14, 1);
|
||||
HashMap<String, QueryResponseWriter> m= new HashMap<>(15, 1);
|
||||
m.put("xml", new XMLResponseWriter());
|
||||
m.put("standard", m.get("xml"));
|
||||
m.put(CommonParams.JSON, new JSONResponseWriter());
|
||||
m.put("geojson", new GeoJSONResponseWriter());
|
||||
m.put("python", new PythonResponseWriter());
|
||||
m.put("php", new PHPResponseWriter());
|
||||
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 {
|
||||
protected String wrapperFunction;
|
||||
private String namedListStyle;
|
||||
private String wrapperFunction;
|
||||
|
||||
private static final String JSON_NL_STYLE="json.nl";
|
||||
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( "json", new RawValueTransformerFactory("json") );
|
||||
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
|
||||
public void write(TextResponseWriter writer, String name, IndexableField f) throws IOException {
|
||||
writer.writeStr(name, f.stringValue(), true);
|
||||
|
|
|
@ -70,6 +70,7 @@
|
|||
<field name="bbox" type="bbox" />
|
||||
|
||||
<dynamicField name="bboxD_*" type="bbox" indexed="true" />
|
||||
<dynamicField name="str_*" type="string" indexed="true" stored="true"/>
|
||||
|
||||
</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