/* * 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() ); } } } }