You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ry...@apache.org on 2016/03/16 18:06:50 UTC
lucene-solr:branch_6x: SOLR-8814: Support GeoJSON response format
Repository: lucene-solr
Updated Branches:
refs/heads/branch_6x cb85f2611 -> 5731331be
SOLR-8814: Support GeoJSON response format
Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/5731331b
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/5731331b
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/5731331b
Branch: refs/heads/branch_6x
Commit: 5731331be1f5fcef829950fcfa9edcb3632babae
Parents: cb85f26
Author: Ryan McKinley <ry...@apache.org>
Authored: Wed Mar 16 09:49:46 2016 -0700
Committer: Ryan McKinley <ry...@apache.org>
Committed: Wed Mar 16 09:49:46 2016 -0700
----------------------------------------------------------------------
solr/CHANGES.txt | 12 +
.../src/java/org/apache/solr/core/SolrCore.java | 3 +-
.../solr/response/GeoJSONResponseWriter.java | 345 +++++++++++++++++++
.../solr/response/JSONResponseWriter.java | 2 +-
.../transform/GeoTransformerFactory.java | 224 ++++++++++++
.../response/transform/TransformerFactory.java | 1 +
.../response/transform/WriteableGeoJSON.java | 55 +++
.../solr/schema/AbstractSpatialFieldType.java | 7 +
.../solr/collection1/conf/schema-spatial.xml | 1 +
.../response/TestGeoJSONResponseWriter.java | 279 +++++++++++++++
10 files changed, 927 insertions(+), 2 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5731331b/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 979b95f..6adee44 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -31,6 +31,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
----------------------
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5731331b/solr/core/src/java/org/apache/solr/core/SolrCore.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/core/SolrCore.java b/solr/core/src/java/org/apache/solr/core/SolrCore.java
index 7a65a72..c5e54d2 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrCore.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrCore.java
@@ -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());
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5731331b/solr/core/src/java/org/apache/solr/response/GeoJSONResponseWriter.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/response/GeoJSONResponseWriter.java b/solr/core/src/java/org/apache/solr/response/GeoJSONResponseWriter.java
new file mode 100644
index 0000000..896be92
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/response/GeoJSONResponseWriter.java
@@ -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();
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5731331b/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java b/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java
index cf894b8..d257f57 100644
--- a/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java
@@ -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";
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5731331b/solr/core/src/java/org/apache/solr/response/transform/GeoTransformerFactory.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/response/transform/GeoTransformerFactory.java b/solr/core/src/java/org/apache/solr/response/transform/GeoTransformerFactory.java
new file mode 100644
index 0000000..7b7974b
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/response/transform/GeoTransformerFactory.java
@@ -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() );
+ }
+ }
+ }
+}
+
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5731331b/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java b/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java
index a600adf..6e7a3dd 100644
--- a/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java
+++ b/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java
@@ -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() );
}
}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5731331b/solr/core/src/java/org/apache/solr/response/transform/WriteableGeoJSON.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/response/transform/WriteableGeoJSON.java b/solr/core/src/java/org/apache/solr/response/transform/WriteableGeoJSON.java
new file mode 100644
index 0000000..40acebf
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/response/transform/WriteableGeoJSON.java
@@ -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);
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5731331b/solr/core/src/java/org/apache/solr/schema/AbstractSpatialFieldType.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/schema/AbstractSpatialFieldType.java b/solr/core/src/java/org/apache/solr/schema/AbstractSpatialFieldType.java
index 222f0b8..e5fd8c6 100644
--- a/solr/core/src/java/org/apache/solr/schema/AbstractSpatialFieldType.java
+++ b/solr/core/src/java/org/apache/solr/schema/AbstractSpatialFieldType.java
@@ -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);
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5731331b/solr/core/src/test-files/solr/collection1/conf/schema-spatial.xml
----------------------------------------------------------------------
diff --git a/solr/core/src/test-files/solr/collection1/conf/schema-spatial.xml b/solr/core/src/test-files/solr/collection1/conf/schema-spatial.xml
index 2c1ca1f..15837f3 100644
--- a/solr/core/src/test-files/solr/collection1/conf/schema-spatial.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/schema-spatial.xml
@@ -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>
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5731331b/solr/core/src/test/org/apache/solr/response/TestGeoJSONResponseWriter.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/response/TestGeoJSONResponseWriter.java b/solr/core/src/test/org/apache/solr/response/TestGeoJSONResponseWriter.java
new file mode 100644
index 0000000..191136b
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/response/TestGeoJSONResponseWriter.java
@@ -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());
+ }
+ }
+ }
+}