Skip to content

Commit 204caf8

Browse files
authored
Add support for a new output format : GeoJSON (#62)
1 parent a363023 commit 204caf8

File tree

7 files changed

+558
-8
lines changed

7 files changed

+558
-8
lines changed

README.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Elasticsearch Data Format Plugin
44
## Overview
55

66
Elasticsearch Data Format Plugin provides a feature to allow you to download a response of a search result as several formats other than JSON.
7-
The supported formats are CSV, Excel, JSON(Bulk) and JSON(Object List).
7+
The supported formats are CSV, Excel, JSON(Bulk), JSON(Object List) and GeoJSON.
88

99
## Version
1010

@@ -31,7 +31,7 @@ If not, it's as scan query(all data are stored.).
3131
| Request Parameter | Type | Description |
3232
|:------------------|:-------:|:------------|
3333
| append.header | boolean | Append column headers if true |
34-
| fields_name | string | choose the fields to dump |
34+
| fields_name | string | choose the fields to dump (comma separate format) |
3535
| source | string | [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html) |
3636
| csv.separator | string | Separate character in CSV |
3737
| csv.quote | string | Quote character in CSV|
@@ -46,7 +46,7 @@ If not, it's as scan query(all data are stored.).
4646
| Request Parameter | Type | Description |
4747
|:------------------|:-------:|:------------|
4848
| append.header | boolean | Append column headers if true |
49-
| fields_name | string | choose the fields to dump |
49+
| fields_name | string | choose the fields to dump (comma separate format) |
5050
| source | string | [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html) |
5151

5252
### Excel 2007
@@ -55,6 +55,8 @@ If not, it's as scan query(all data are stored.).
5555

5656
| Request Parameter | Type | Description |
5757
|:------------------|:-------:|:------------|
58+
| append.header | boolean | Append column headers if true |
59+
| fields_name | string | choose the fields to dump (comma separate format) |
5860
| source | string | [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html) |
5961

6062
### JSON (Elasticsearch Bulk format)
@@ -75,3 +77,19 @@ If not, it's as scan query(all data are stored.).
7577
| :---------------- | :----: | :----------------------------------------------------------- |
7678
| source | string | [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html) |
7779

80+
### GeoJSON (Open GIS standard)
81+
82+
$ curl -o /tmp/data.json -XGET "localhost:9200/{index}/{type}/_data?format=geojson&source=..."
83+
84+
| Request Parameter | Type | Description |
85+
| :----------------------- | :----: | :----------------------------------------------------------- |
86+
| source | string | [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html) |
87+
| geometry.lon_field | string | Longitude field for coordinates (Support Geometry type "Point") |
88+
| geometry.lat_field | string | Latitude field for coordinates (Support Geometry type "Point") |
89+
| geometry.alt_field | string | Altitude field for coordinates (Support Geometry type "Point") |
90+
| geometry.coord_field | string | Coordinates field. Support all Geometry types (see [GeoJSON Example](https://en.wikipedia.org/wiki/GeoJSON)).<br/>If set, overwrite `geometry.lon_field`, `geometry.lat_field` and `geometry.alt_field` |
91+
| geometry.type_field | string | Geometry type field (see [GeoJSON Example](https://en.wikipedia.org/wiki/GeoJSON))<br/>Only used if `geometry.coord_field` param is set |
92+
| keep_geometry_info | boolean | Keep or not the original geometry fields in final GeoJSON properties (default: false) |
93+
| exclude_fields | string | Exclude fields in final geojson properties (comma separate format) |
94+
95+
**NB**: Field name can use basic style like `a` or JSONpath style like `a.b.c[2].d`

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@
121121
<artifactId>poi-ooxml-schemas</artifactId>
122122
<version>${poi.version}</version>
123123
</dependency>
124+
<dependency>
125+
<groupId>com.google.code.gson</groupId>
126+
<artifactId>gson</artifactId>
127+
<version>2.8.6</version>
128+
</dependency>
124129
<dependency>
125130
<groupId>org.codelibs</groupId>
126131
<artifactId>elasticsearch-cluster-runner</artifactId>

src/main/java/org/codelibs/elasticsearch/df/content/ContentType.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.codelibs.elasticsearch.df.content;
22

33
import org.codelibs.elasticsearch.df.content.csv.CsvContent;
4+
import org.codelibs.elasticsearch.df.content.geojson.GeoJsonContent;
45
import org.codelibs.elasticsearch.df.content.json.JsonContent;
56
import org.codelibs.elasticsearch.df.content.json.JsonListContent;
67
import org.codelibs.elasticsearch.df.content.xls.XlsContent;
@@ -117,6 +118,27 @@ public String fileName(final RestRequest request) {
117118
}
118119
return index + ".json";
119120
}
121+
},
122+
GEOJSON(60) {
123+
@Override
124+
public String contentType() {
125+
return "application/geo+json";
126+
}
127+
128+
@Override
129+
public DataContent dataContent(final Client client,
130+
final RestRequest request) {
131+
return new GeoJsonContent(client, request, this);
132+
}
133+
134+
@Override
135+
public String fileName(final RestRequest request) {
136+
final String index = request.param("index");
137+
if (index == null) {
138+
return "_all.geojson";
139+
}
140+
return index + ".geojson";
141+
}
120142
};
121143

122144
private int index;
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
package org.codelibs.elasticsearch.df.content.geojson;
2+
3+
import java.io.BufferedWriter;
4+
import java.io.File;
5+
import java.io.FileOutputStream;
6+
import java.io.IOException;
7+
import java.io.OutputStreamWriter;
8+
import java.io.Writer;
9+
import java.util.ArrayList;
10+
import java.util.Collections;
11+
import java.util.List;
12+
13+
import org.apache.logging.log4j.LogManager;
14+
import org.apache.logging.log4j.Logger;
15+
import org.codelibs.elasticsearch.df.content.ContentType;
16+
import org.codelibs.elasticsearch.df.content.DataContent;
17+
import org.codelibs.elasticsearch.df.util.JsonUtils;
18+
import org.codelibs.elasticsearch.df.util.RequestUtil;
19+
import org.codelibs.elasticsearch.df.util.StringUtils;
20+
import org.elasticsearch.ElasticsearchException;
21+
import org.elasticsearch.action.ActionListener;
22+
import org.elasticsearch.action.search.SearchResponse;
23+
import org.elasticsearch.client.Client;
24+
import org.elasticsearch.common.xcontent.XContentHelper;
25+
import org.elasticsearch.common.xcontent.XContentType;
26+
import org.elasticsearch.rest.RestChannel;
27+
import org.elasticsearch.rest.RestRequest;
28+
import org.elasticsearch.search.SearchHit;
29+
import org.elasticsearch.search.SearchHits;
30+
31+
import com.google.gson.Gson;
32+
import com.google.gson.GsonBuilder;
33+
import com.google.gson.JsonArray;
34+
import com.google.gson.JsonElement;
35+
import com.google.gson.JsonObject;
36+
import com.google.gson.JsonParser;
37+
38+
public class GeoJsonContent extends DataContent {
39+
private static final Logger logger = LogManager.getLogger(GeoJsonContent.class);
40+
41+
private final String geometryCoordinatesLonField;
42+
private final String geometryCoordinatesLatField;
43+
private final String geometryCoordinatesAltField;
44+
private final String geometryTypeField;
45+
private final String geometryCoordinatesField;
46+
private final boolean geometryKeepGeoInfo;
47+
private final List<String> excludeFields;
48+
49+
public GeoJsonContent(final Client client, final RestRequest request, final ContentType contentType) {
50+
super(client, request, contentType);
51+
52+
geometryCoordinatesLonField = request.param("geometry.lon_field",StringUtils.EMPTY_STRING);
53+
geometryCoordinatesLatField = request.param("geometry.lat_field",StringUtils.EMPTY_STRING);
54+
geometryCoordinatesAltField = request.param("geometry.alt_field",StringUtils.EMPTY_STRING);
55+
geometryTypeField = request.param("geometry.type_field",StringUtils.EMPTY_STRING);
56+
geometryCoordinatesField = request.param("geometry.coord_field",StringUtils.EMPTY_STRING);
57+
geometryKeepGeoInfo = request.paramAsBoolean("keep_geometry_info",false);
58+
59+
final String[] fields = request.paramAsStringArray("exclude_fields", StringUtils.EMPTY_STRINGS);
60+
if (fields.length == 0) {
61+
excludeFields = new ArrayList<>();
62+
} else {
63+
final List<String> fieldList = new ArrayList<>();
64+
for (final String field : fields) {
65+
fieldList.add(field.trim());
66+
}
67+
excludeFields = Collections.unmodifiableList(fieldList);
68+
}
69+
70+
if (logger.isDebugEnabled()) {
71+
logger.debug("geometryTypeField: {}, geometryCoordinatesField: {}, geometryCoordinatesLonField: {}, " +
72+
"geometryCoordinatesLatField: {}, geometryCoordinatesAltField: {}, geometryKeepGeoInfo: {}, excludeFields: {}",
73+
geometryTypeField, geometryCoordinatesField, geometryCoordinatesLonField,
74+
geometryCoordinatesLatField, geometryCoordinatesAltField, geometryKeepGeoInfo, excludeFields);
75+
}
76+
}
77+
78+
@Override
79+
public void write(final File outputFile, final SearchResponse response, final RestChannel channel,
80+
final ActionListener<Void> listener) {
81+
try {
82+
final OnLoadListener onLoadListener = new OnLoadListener(
83+
outputFile, listener);
84+
onLoadListener.onResponse(response);
85+
} catch (final Exception e) {
86+
listener.onFailure(new ElasticsearchException("Failed to write data.",
87+
e));
88+
}
89+
}
90+
91+
protected class OnLoadListener implements ActionListener<SearchResponse> {
92+
protected ActionListener<Void> listener;
93+
94+
protected Writer writer;
95+
96+
protected File outputFile;
97+
98+
private long currentCount = 0;
99+
100+
private boolean firstLine = true;
101+
102+
protected OnLoadListener(final File outputFile, final ActionListener<Void> listener) {
103+
this.outputFile = outputFile;
104+
this.listener = listener;
105+
try {
106+
writer = new BufferedWriter(new OutputStreamWriter(
107+
new FileOutputStream(outputFile), "UTF-8"));
108+
} catch (final Exception e) {
109+
throw new ElasticsearchException("Could not open "
110+
+ outputFile.getAbsolutePath(), e);
111+
}
112+
try {
113+
writer.append("{\"type\": \"FeatureCollection\", \"features\": [");
114+
}catch (final Exception e) {
115+
onFailure(e);
116+
}
117+
}
118+
119+
@Override
120+
public void onResponse(final SearchResponse response) {
121+
final Gson gsonWriter = new GsonBuilder().create();
122+
final String scrollId = response.getScrollId();
123+
final SearchHits hits = response.getHits();
124+
final int size = hits.getHits().length;
125+
currentCount += size;
126+
if (logger.isDebugEnabled()) {
127+
logger.debug("scrollId: {}, totalHits: {}, hits: {}, current: {}",
128+
scrollId, hits.getTotalHits(), size, currentCount);
129+
}
130+
try {
131+
for (final SearchHit hit : hits) {
132+
final String source = XContentHelper.convertToJson(
133+
hit.getSourceRef(), true, false, XContentType.JSON);
134+
if (!firstLine){
135+
writer.append(',');
136+
}else{
137+
firstLine = false;
138+
}
139+
140+
final JsonElement propertiesJson = JsonParser.parseString(source);
141+
String geometryType = "";
142+
143+
JsonArray geometryCoordinates = new JsonArray();
144+
if (!geometryCoordinatesField.isEmpty()){
145+
JsonElement jsonEltCoord = JsonUtils.getJsonElement(propertiesJson,geometryCoordinatesField);
146+
if (jsonEltCoord !=null && !jsonEltCoord.isJsonNull()){
147+
geometryCoordinates = jsonEltCoord.getAsJsonArray​();
148+
if (!geometryKeepGeoInfo){
149+
JsonUtils.removeJsonElement(propertiesJson,geometryCoordinatesField);
150+
}
151+
}
152+
if (!geometryTypeField.isEmpty()){
153+
JsonElement jsonEltType = JsonUtils.getJsonElement(propertiesJson,geometryTypeField);
154+
if (jsonEltType !=null && !jsonEltType.isJsonNull()){
155+
geometryType = jsonEltType.getAsString();
156+
if (!geometryKeepGeoInfo){
157+
JsonUtils.removeJsonElement(propertiesJson,geometryTypeField);
158+
}
159+
}
160+
}
161+
}else{
162+
if (!geometryCoordinatesLonField.isEmpty() && !geometryCoordinatesLatField.isEmpty()){
163+
JsonElement jsonEltLon = JsonUtils.getJsonElement(propertiesJson,geometryCoordinatesLonField);
164+
JsonElement jsonEltLat = JsonUtils.getJsonElement(propertiesJson,geometryCoordinatesLatField);
165+
if (jsonEltLon !=null && !jsonEltLon.isJsonNull() && jsonEltLat !=null && !jsonEltLat.isJsonNull()){
166+
geometryCoordinates.add(jsonEltLon.getAsNumber());
167+
geometryCoordinates.add(jsonEltLat.getAsNumber());
168+
if (!geometryKeepGeoInfo) {
169+
JsonUtils.removeJsonElement(propertiesJson,geometryCoordinatesLonField);
170+
JsonUtils.removeJsonElement(propertiesJson,geometryCoordinatesLatField);
171+
}
172+
}
173+
}
174+
if (!geometryCoordinatesAltField.isEmpty()){
175+
JsonElement jsonElt = JsonUtils.getJsonElement(propertiesJson,geometryCoordinatesAltField);
176+
if (jsonElt !=null && !jsonElt.isJsonNull()){
177+
geometryCoordinates.add(jsonElt.getAsNumber());
178+
if (!geometryKeepGeoInfo) {
179+
JsonUtils.removeJsonElement(propertiesJson,geometryCoordinatesAltField);
180+
}
181+
}
182+
}
183+
geometryType = "Point";
184+
}
185+
186+
for (String excludeField : excludeFields) {
187+
JsonUtils.removeJsonElement(propertiesJson,excludeField);
188+
}
189+
190+
JsonObject geometryObject = new JsonObject();
191+
geometryObject.addProperty("type", geometryType);
192+
geometryObject.add("coordinates", geometryCoordinates);
193+
194+
JsonObject featureObject = new JsonObject();
195+
featureObject.addProperty("type", "Feature");
196+
featureObject.add("geometry", geometryObject);
197+
featureObject.add("properties", propertiesJson.getAsJsonObject());
198+
199+
writer.append('\n').append(gsonWriter.toJson(featureObject));
200+
}
201+
202+
if (size == 0 || scrollId == null) {
203+
// end
204+
writer.append('\n').append("]}");
205+
writer.flush();
206+
close();
207+
listener.onResponse(null);
208+
} else {
209+
client.prepareSearchScroll(scrollId)
210+
.setScroll(RequestUtil.getScroll(request))
211+
.execute(this);
212+
}
213+
} catch (final Exception e) {
214+
onFailure(e);
215+
}
216+
}
217+
218+
@Override
219+
public void onFailure(final Exception e) {
220+
try {
221+
close();
222+
} catch (final Exception e1) {
223+
// ignore
224+
}
225+
listener.onFailure(new ElasticsearchException("Failed to write data.",
226+
e));
227+
}
228+
229+
private void close() {
230+
if (writer != null) {
231+
try {
232+
writer.close();
233+
} catch (final IOException e) {
234+
throw new ElasticsearchException("Could not close "
235+
+ outputFile.getAbsolutePath(), e);
236+
}
237+
}
238+
}
239+
}
240+
}

src/main/java/org/codelibs/elasticsearch/df/rest/RestDataAction.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ private ContentType getContentType(final RestRequest request) {
132132
} else if ("application/list+json".equals(contentType)
133133
|| "jsonlist".equals(contentType)) {
134134
return ContentType.JSONLIST;
135+
} else if ("application/geo+json".equals(contentType)
136+
|| "application/geojson".equals(contentType)
137+
|| "geojson".equals(contentType)) {
138+
return ContentType.GEOJSON;
135139
}
136140

137141
return null;

0 commit comments

Comments
 (0)