diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index 979b95f2a08..6adee44b561 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= + 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= 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 ---------------------- 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 7a65a72decb..c5e54d21a0d 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 responseWriters = new PluginBag<>(QueryResponseWriter.class, this); public static final Map DEFAULT_RESPONSE_WRITERS ; static{ - HashMap m= new HashMap<>(14, 1); + HashMap 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()); 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 00000000000..896be92f8f6 --- /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 geojson.org + */ +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 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 childDocs = doc.getChildDocuments(); + for(int i=0; i + *
  • GeoJSON
  • + *
  • WKT
  • + *
  • Polyshape
  • + * + * For more information see: spatial4j/FORMATS.md + * + * The shape is either read from a stored field, or a ValueSource. + * + * This transformer is useful when: + *
      + *
    • You want to return a format different than the stored encoding (WKT vs GeoJSON)
    • + *
    • The {@link Shape} is stored in a {@link ValueSource}, not a stored field
    • + *
    • the value is not stored in a format the output understands (ie, raw GeoJSON)
    • + *
    + * + */ +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() ); + } + } + } +} + 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 a600adfcc4a..6e7a3dd986e 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() ); } } 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 00000000000..40acebf2a7b --- /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); + } +} 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 222f0b83eb6..e5fd8c63d95 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 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); 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 2c1ca1f943b..15837f348ae 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 @@ + 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 00000000000..191136bc2a3 --- /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() { + + +// +// +// +// + + // 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 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 getFirstFeatureGeometry(Map json) + { + Map rsp = (Map)json.get("response"); + assertEquals("FeatureCollection", rsp.get("type")); + List vals = (List)rsp.get("features"); + assertEquals(1, vals.size()); + Map feature = (Map)vals.get(0); + assertEquals("Feature", feature.get("type")); + return (Map)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 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 json = readJSON(h.query(req( + "q","id:H.B", + "wt","geojson", + "fl","*", + "geojson.field", "srpt_geohash", + "indent","true"))); + + Map 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 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 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()); + } + } + } +}