├── .gitignore
├── README.textile
├── pom.xml
└── src
└── main
├── assemblies
└── plugin.xml
├── java
├── co
│ └── diji
│ │ ├── rest
│ │ ├── SolrSearchHandlerRestAction.java
│ │ └── SolrUpdateHandlerRestAction.java
│ │ ├── solr
│ │ ├── SolrResponseWriter.java
│ │ └── XMLWriter.java
│ │ └── utils
│ │ └── QueryStringDecoder.java
└── org
│ └── elasticsearch
│ └── plugin
│ └── diji
│ └── MockSolrPlugin.java
└── resources
└── es-plugin.properties
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | .settings
3 | .classpath
4 | .project
5 | .idea
6 |
--------------------------------------------------------------------------------
/README.textile:
--------------------------------------------------------------------------------
1 | h1. ElasticSearch Mock Solr Plugin
2 |
3 | |_.Mock Solr Plugin|_.elasticsearch|_.Lucene/Solr|
4 | |master|0.20.2 -> 0.20.X|3.6.2|
5 | |1.1.4|0.20.2 -> 0.20.X|3.6.2|
6 | |1.1.3|0.19.3 -> 0.20.1|3.6.0|
7 | |1.1.2|0.19.0 -> 0.19.2|3.5.0|
8 | |1.1.1|0.18.6 -> 0.18.7|3.5.0|
9 | |1.1.0|0.18.0 -> 0.18.5|3.5.0|
10 |
11 | h2. Use Solr clients/tools with ElasticSearch
12 |
13 | This plugin will allow you to use tools that were built to
14 | interact with Solr with ElasticSearch.
15 |
16 | The idea for this plugin came when I wanted to use Nutch with
17 | ElasticSearch. Instead of extending Nutch itself,
18 | I thought it would be nice to use any Solr clients with
19 | ElasticSearch. Some projects we can now use are
20 | Nutch, Apache ManifoldCF, and any tool using SolrJ. It
21 | should be possible to use non-java tools that write to
22 | Solr using the XML update and request handlers as well.
23 |
24 | h3. Supported Solr features
25 |
26 | * Update handlers
27 | ** XML Update Handler (ie. /update)
28 | ** JavaBin Update Handler (ie. /update/javabin)
29 | * Search handler (ie. /select)
30 | ** Basic lucene queries using the q paramter
31 | ** start, rows, and fl parameters
32 | ** sorting
33 | ** filter queries (fq parameters)
34 | ** hit highlighting (hl, hl.fl, hl.snippets, hl.fragsize, hl.simple.pre, hl.simple.post)
35 | ** faceting (facet, facet.field, facet.query, facet.sort, facet.limit)
36 | * XML and JavaBin request and response formats
37 |
38 | h3. How do you build this plugin?
39 |
40 | Use maven to build the package
41 |
42 |
43 | mvn package
44 |
45 |
46 | Then install the plugin
47 |
48 |
49 | # if you've built it locally
50 | $ES_HOME/bin/plugin -url file:./target/releases/elasticsearch-mocksolrplugin-*.zip -install mocksolrplugin
51 |
52 |
53 | h3. How to use this plugin.
54 |
55 | Just point your Solr client/tool to your ElasticSearch instance and appending
56 | /_solr to the url.
57 |
58 | http://localhost:9200/${index}/${type}/_solr
59 |
60 | ${index} - the ES index you want to index/search against. Default "solr".
61 | ${type} - the ES type you want to index/search against. Default "docs".
62 |
63 | Example paths:
64 |
65 | // Will search/index against index "solr" and type "docs"
66 | http://localhost:9200/_solr
67 |
68 | // Will search/index against index "testindex" and type "docs"
69 | http://localhost:9200/testindex/_solr
70 |
71 | // Will search/index against index "testindex" and type "testtype"
72 | http://localhost:9200/testindex/testtype/_solr
73 |
74 |
75 | Use the client/tool as you would with Solr.
76 |
77 | h3. Example SolrJ Indexing
78 |
79 |
80 | CommonsHttpSolrServer server = new CommonsHttpSolrServer("http://localhost:9200/testindex/testtype/_solr");
81 | server.setRequestWriter(new BinaryRequestWriter());
82 | // we support both xml and SolrBin response writers
83 | //server.setParser(new XMLResponseParser());
84 |
85 | SolrInputDocument doc1 = new SolrInputDocument();
86 | doc1.addField( "id", "id1", 1.0f );
87 | doc1.addField( "name", "doc1", 1.0f );
88 | doc1.addField( "price", 10 );
89 |
90 | SolrInputDocument doc2 = new SolrInputDocument();
91 | doc2.addField( "id", "id2", 1.0f );
92 | doc2.addField( "name", "doc2", 1.0f );
93 | doc2.addField( "price", 20 );
94 |
95 | Collection docs = new ArrayList();
96 | docs.add( doc1 );
97 | docs.add( doc2 );
98 |
99 | server.add( docs );
100 | server.commit();
101 |
102 | // deletes work as well
103 | //server.deleteById("id2");
104 | //server.commit();
105 |
106 |
107 | Perform a search and verify the documents were indexed.
108 |
109 | h3. Example SolrJ Searching
110 |
111 |
112 | CommonsHttpSolrServer server = new CommonsHttpSolrServer("http://localhost:9200/testindex/testtype/_solr");
113 |
114 | String qstr = "id:[* TO *]";
115 | SolrQuery query = new SolrQuery();
116 | query.setQuery(qstr);
117 |
118 | QueryResponse response = server.query(query);
119 | for (SolrDocument doc : response.getResults()) {
120 | for (String field : doc.getFieldNames()) {
121 | System.out.println(field + " = " + doc.getFieldValue(field));
122 | }
123 | System.out.println();
124 | }
125 |
126 |
127 |
128 | h3. Example using Nutch
129 |
130 | At a minimum, use the following type mapping for ElasticSearch.
131 |
132 |
133 | curl -XPUT 'http://localhost:9200/testindex'
134 | curl -XPUT 'http://localhost:9200/testindex/testtype/_mapping' -d '{
135 | "testtype" : {
136 | "properties" : {
137 | "id" : {
138 | "type" : "string",
139 | "store": "yes"
140 | },
141 | "digest" : {
142 | "type" : "string",
143 | "store" : "yes",
144 | "index" : "no"
145 | },
146 | "boost" : {
147 | "type" : "float",
148 | "store" : "yes",
149 | "index" : "no"
150 | },
151 | "tstamp" : {
152 | "type" : "date",
153 | "store" : "yes",
154 | "index" : "no"
155 | }
156 | }
157 | }
158 | }'
159 |
160 |
161 | Follow the nutch tutorial at http://wiki.apache.org/nutch/NutchTutorial
162 | * Follow steps 1 though 3.1
163 | * For step 3.1 use:
164 |
165 |
166 | bin/nutch crawl urls -solr http://localhost:9200/testindex/testtype/_solr -depth 3 -topN 5
167 |
168 |
169 | h3. Notes
170 |
171 | ElasticSearch does not require a schema and all the data you send to Solr will be indexed by default. You
172 | Can use the ElasticSearch PUT Mapping API to define your field types, what should be stored, analyzed, etc.
173 | All data that is indexed via the mock XML Update Handler will most likely be detected by ElasticSearch as
174 | strings, thus it is a good idea to mimic your Solr schema with an ElasticSearch type mapping.
175 |
176 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | elasticsearch-mocksolrplugin
4 | 4.0.0
5 | co.diji
6 | elasticsearch-mocksolrplugin
7 | 1.1.5-SNAPSHOT
8 | jar
9 | Use Solr clients/tools with ElasticSearch
10 | 2011
11 |
12 |
13 | The Apache Software License, Version 2.0
14 | http://www.apache.org/licenses/LICENSE-2.0.txt
15 | repo
16 |
17 |
18 |
19 | scm:git:git@github.com:mattweber/elasticsearch-mocksolrplugin.git
20 | scm:git:git@github.com:mattweber/elasticsearch-mocksolrplugin.git
21 | https://github.com/mattweber/elasticsearch-mocksolrplugin
22 |
23 |
24 |
25 | org.sonatype.oss
26 | oss-parent
27 | 7
28 |
29 |
30 |
31 | 0.20.5
32 | 3.6.2
33 |
34 |
35 |
36 |
37 | oss.sonatype.org
38 | OSS Sonatype
39 | http://oss.sonatype.org/content/repositories/releases/
40 |
41 |
42 |
43 |
44 |
45 | org.elasticsearch
46 | elasticsearch
47 | ${elasticsearch.version}
48 | compile
49 |
50 |
51 |
52 | org.apache.solr
53 | solr-solrj
54 | ${solr.version}
55 | compile
56 |
57 |
58 | commons-logging
59 | commons-logging
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | org.apache.maven.plugins
69 | maven-compiler-plugin
70 | 2.3.2
71 |
72 | 1.6
73 | 1.6
74 |
75 |
76 |
77 | org.apache.maven.plugins
78 | maven-surefire-plugin
79 | 2.11
80 |
81 |
82 | **/*Tests.java
83 |
84 |
85 |
86 |
87 | org.apache.maven.plugins
88 | maven-source-plugin
89 | 2.1.2
90 |
91 |
92 | attach-sources
93 |
94 | jar
95 |
96 |
97 |
98 |
99 |
100 | maven-assembly-plugin
101 |
102 | ${project.build.directory}/releases/
103 |
104 | ${basedir}/src/main/assemblies/plugin.xml
105 |
106 |
107 |
108 |
109 | package
110 |
111 | single
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
--------------------------------------------------------------------------------
/src/main/assemblies/plugin.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | zip
6 |
7 | false
8 |
9 |
10 | /
11 | true
12 | true
13 |
14 | org.elasticsearch:elasticsearch
15 |
16 |
17 |
18 | /
19 | true
20 | true
21 |
22 | org.apache.solr:solr-solrj
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/main/java/co/diji/rest/SolrSearchHandlerRestAction.java:
--------------------------------------------------------------------------------
1 | package co.diji.rest;
2 |
3 | import co.diji.solr.SolrResponseWriter;
4 | import co.diji.utils.QueryStringDecoder;
5 | import org.apache.solr.common.SolrDocument;
6 | import org.apache.solr.common.SolrDocumentList;
7 | import org.apache.solr.common.util.NamedList;
8 | import org.apache.solr.common.util.SimpleOrderedMap;
9 | import org.elasticsearch.action.ActionListener;
10 | import org.elasticsearch.action.search.SearchRequest;
11 | import org.elasticsearch.action.search.SearchResponse;
12 | import org.elasticsearch.client.Client;
13 | import org.elasticsearch.common.Strings;
14 | import org.elasticsearch.common.inject.Inject;
15 | import org.elasticsearch.common.joda.time.format.DateTimeFormatter;
16 | import org.elasticsearch.common.joda.time.format.ISODateTimeFormat;
17 | import org.elasticsearch.common.settings.Settings;
18 | import org.elasticsearch.index.query.AndFilterBuilder;
19 | import org.elasticsearch.index.query.FilterBuilder;
20 | import org.elasticsearch.index.query.QueryBuilder;
21 | import org.elasticsearch.index.query.QueryBuilders;
22 | import org.elasticsearch.rest.*;
23 | import org.elasticsearch.rest.action.support.RestActions;
24 | import org.elasticsearch.search.SearchHit;
25 | import org.elasticsearch.search.SearchHitField;
26 | import org.elasticsearch.search.SearchHits;
27 | import org.elasticsearch.search.builder.SearchSourceBuilder;
28 | import org.elasticsearch.search.facet.Facet;
29 | import org.elasticsearch.search.facet.query.QueryFacet;
30 | import org.elasticsearch.search.facet.query.QueryFacetBuilder;
31 | import org.elasticsearch.search.facet.terms.TermsFacet;
32 | import org.elasticsearch.search.facet.terms.TermsFacetBuilder;
33 | import org.elasticsearch.search.highlight.HighlightBuilder;
34 | import org.elasticsearch.search.highlight.HighlightField;
35 | import org.elasticsearch.search.sort.SortOrder;
36 |
37 | import java.io.IOException;
38 | import java.util.Iterator;
39 | import java.util.List;
40 | import java.util.Map;
41 | import java.util.regex.Pattern;
42 |
43 | import static org.elasticsearch.index.query.FilterBuilders.andFilter;
44 | import static org.elasticsearch.index.query.FilterBuilders.queryFilter;
45 |
46 | public class SolrSearchHandlerRestAction extends BaseRestHandler {
47 |
48 | // handles solr response formats
49 | private final SolrResponseWriter solrResponseWriter = new SolrResponseWriter();
50 |
51 | // regex and date format to detect ISO8601 date formats
52 | private final Pattern datePattern = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z");;
53 | private final DateTimeFormatter dateFormat = ISODateTimeFormat.dateOptionalTimeParser();
54 |
55 | /**
56 | * Rest actions that mocks the Solr search handler
57 | *
58 | * @param settings ES settings
59 | * @param client ES client
60 | * @param restController ES rest controller
61 | */
62 | @Inject
63 | public SolrSearchHandlerRestAction(Settings settings, Client client, RestController restController) {
64 | super(settings, client);
65 |
66 | // register search handler
67 | // specifying and index and type is optional
68 | restController.registerHandler(RestRequest.Method.GET, "/_solr/select", this);
69 | restController.registerHandler(RestRequest.Method.GET, "/{index}/_solr/select", this);
70 | restController.registerHandler(RestRequest.Method.GET, "/{index}/{type}/_solr/select", this);
71 | }
72 |
73 | /**
74 | * Parse uri parameters.
75 | *
76 | * ES request.param does not support multiple parameters with the same name yet. This
77 | * is needed for parameters such as fq in Solr. This will not be needed once a fix is
78 | * in ES. https://github.com/elasticsearch/elasticsearch/issues/1544
79 | *
80 | * @param uri The uri to parse
81 | * @return a map of parameters, each parameter value is a list of strings.
82 | */
83 | private Map> parseUriParams(String uri) {
84 | // use netty query string decoder
85 | QueryStringDecoder decoder = new QueryStringDecoder(uri);
86 | return decoder.getParameters();
87 | }
88 |
89 | /*
90 | * (non-Javadoc)
91 | *
92 | * @see
93 | * org.elasticsearch.rest.RestHandler#handleRequest(org.elasticsearch.rest.RestRequest, org.elasticsearch.rest.RestChannel)
94 | */
95 | public void handleRequest(final RestRequest request, final RestChannel channel) {
96 | // Get the parameters
97 | final Map> params = parseUriParams(request.uri());
98 |
99 | // generate the search request
100 | SearchRequest searchRequest = getSearchRequest(params, request);
101 | searchRequest.listenerThreaded(false);
102 |
103 | // execute the search
104 | client.search(searchRequest, new ActionListener() {
105 | @Override
106 | public void onResponse(SearchResponse response) {
107 | try {
108 | // write response
109 | solrResponseWriter.writeResponse(createSearchResponse(params, request, response), request, channel);
110 | } catch (Exception e) {
111 | onFailure(e);
112 | }
113 | }
114 |
115 | @Override
116 | public void onFailure(Throwable e) {
117 | try {
118 | logger.error("Error processing executing search", e);
119 | channel.sendResponse(new XContentThrowableRestResponse(request, e));
120 | } catch (IOException e1) {
121 | logger.error("Failed to send failure response", e1);
122 | }
123 | }
124 | });
125 | }
126 |
127 | /**
128 | * Generates an ES SearchRequest based on the Solr Input Parameters
129 | *
130 | * @param request the ES RestRequest
131 | * @return the generated ES SearchRequest
132 | */
133 | private SearchRequest getSearchRequest(Map> params, RestRequest request) {
134 | // get solr search parameters
135 | String q = request.param("q");
136 | int start = request.paramAsInt("start", 0);
137 | int rows = request.paramAsInt("rows", 10);
138 | String fl = request.param("fl");
139 | String sort = request.param("sort");
140 | List fqs = params.get("fq");
141 | boolean hl = request.paramAsBoolean("hl", false);
142 | boolean facet = request.paramAsBoolean("facet", false);
143 | boolean qDsl = request.paramAsBoolean("q.dsl", false);
144 | boolean fqDsl = request.paramAsBoolean("fq.dsl", false);
145 |
146 | // build the query
147 | SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
148 | if (q != null) {
149 | QueryBuilder queryBuilder;
150 | if (qDsl) {
151 | queryBuilder = QueryBuilders.wrapperQuery(q);
152 | } else {
153 | queryBuilder = QueryBuilders.queryString(q);
154 | }
155 | searchSourceBuilder.query(queryBuilder);
156 | }
157 |
158 | searchSourceBuilder.from(start);
159 | searchSourceBuilder.size(rows);
160 |
161 | // parse fl into individual fields
162 | // solr supports separating by comma or spaces
163 | if (fl != null) {
164 | if (!Strings.hasText(fl)) {
165 | searchSourceBuilder.noFields();
166 | } else {
167 | searchSourceBuilder.fields(fl.split("\\s|,"));
168 | }
169 | }
170 |
171 | // handle sorting
172 | if (sort != null) {
173 | String[] sorts = Strings.splitStringByCommaToArray(sort);
174 | for (int i = 0; i < sorts.length; i++) {
175 | String sortStr = sorts[i].trim();
176 | int delimiter = sortStr.lastIndexOf(" ");
177 | if (delimiter != -1) {
178 | String sortField = sortStr.substring(0, delimiter);
179 | if ("score".equals(sortField)) {
180 | sortField = "_score";
181 | }
182 | String reverse = sortStr.substring(delimiter + 1);
183 | if ("asc".equals(reverse)) {
184 | searchSourceBuilder.sort(sortField, SortOrder.ASC);
185 | } else if ("desc".equals(reverse)) {
186 | searchSourceBuilder.sort(sortField, SortOrder.DESC);
187 | }
188 | } else {
189 | searchSourceBuilder.sort(sortStr);
190 | }
191 | }
192 | } else {
193 | // default sort by descending score
194 | searchSourceBuilder.sort("_score", SortOrder.DESC);
195 | }
196 |
197 | // handler filters
198 | if (fqs != null && !fqs.isEmpty()) {
199 | FilterBuilder filterBuilder = null;
200 |
201 | // if there is more than one filter specified build
202 | // an and filter of query filters, otherwise just
203 | // build a single query filter.
204 | if (fqs.size() > 1) {
205 | AndFilterBuilder fqAnd = andFilter();
206 | for (String fq : fqs) {
207 | QueryBuilder queryBuilder = fqDsl ? QueryBuilders.wrapperQuery(fq) : QueryBuilders.queryString(fq);
208 | fqAnd.add(queryFilter(queryBuilder));
209 | }
210 | filterBuilder = fqAnd;
211 | } else {
212 | QueryBuilder queryBuilder = fqDsl ? QueryBuilders.wrapperQuery(fqs.get(0)) : QueryBuilders.queryString(fqs.get(0));
213 | filterBuilder = queryFilter(queryBuilder);
214 | }
215 |
216 | searchSourceBuilder.filter(filterBuilder);
217 | }
218 |
219 | // handle highlighting
220 | if (hl) {
221 | // get supported highlighting parameters if they exist
222 | String hlfl = request.param("hl.fl");
223 | int hlsnippets = request.paramAsInt("hl.snippets", 1);
224 | int hlfragsize = request.paramAsInt("hl.fragsize", 100);
225 | String hlsimplepre = request.param("hl.simple.pre");
226 | String hlsimplepost = request.param("hl.simple.post");
227 |
228 | HighlightBuilder highlightBuilder = new HighlightBuilder();
229 | if (hlfl == null) {
230 | // run against default _all field
231 | highlightBuilder.field("_all", hlfragsize, hlsnippets);
232 | } else {
233 | String[] hlfls = hlfl.split("\\s|,");
234 | for (String hlField : hlfls) {
235 | // skip wildcarded fields
236 | if (!hlField.contains("*")) {
237 | highlightBuilder.field(hlField, hlfragsize, hlsnippets);
238 | }
239 | }
240 | }
241 |
242 | // pre tags
243 | if (hlsimplepre != null) {
244 | highlightBuilder.preTags(hlsimplepre);
245 | }
246 |
247 | // post tags
248 | if (hlsimplepost != null) {
249 | highlightBuilder.postTags(hlsimplepost);
250 | }
251 |
252 | searchSourceBuilder.highlight(highlightBuilder);
253 |
254 | }
255 |
256 | // handle faceting
257 | if (facet) {
258 | // get supported facet parameters if they exist
259 | List facetFields = params.get("facet.field");
260 | String facetSort = request.param("facet.sort");
261 | int facetLimit = request.paramAsInt("facet.limit", 100);
262 |
263 | List facetQueries = params.get("facet.query");
264 |
265 | if (facetFields != null && !facetFields.isEmpty()) {
266 | for (String facetField : facetFields) {
267 | TermsFacetBuilder termsFacetBuilder = new TermsFacetBuilder(facetField);
268 | termsFacetBuilder.size(facetLimit);
269 | termsFacetBuilder.field(facetField);
270 |
271 | if (facetSort != null && facetSort.equals("index")) {
272 | termsFacetBuilder.order(TermsFacet.ComparatorType.TERM);
273 | } else {
274 | termsFacetBuilder.order(TermsFacet.ComparatorType.COUNT);
275 | }
276 |
277 | searchSourceBuilder.facet(termsFacetBuilder);
278 | }
279 | }
280 |
281 | if (facetQueries != null && !facetQueries.isEmpty()) {
282 | for (String facetQuery : facetQueries) {
283 | QueryFacetBuilder queryFacetBuilder = new QueryFacetBuilder(facetQuery);
284 | queryFacetBuilder.query(QueryBuilders.queryString(facetQuery));
285 | searchSourceBuilder.facet(queryFacetBuilder);
286 | }
287 | }
288 | }
289 |
290 | // get index and type we want to search against
291 | final String index = request.hasParam("index") ? request.param("index") : "solr";
292 | final String type = request.hasParam("type") ? request.param("type") : "docs";
293 |
294 | // Build the search Request
295 | String[] indices = RestActions.splitIndices(index);
296 | SearchRequest searchRequest = new SearchRequest(indices);
297 | searchRequest.extraSource(searchSourceBuilder);
298 | searchRequest.types(RestActions.splitTypes(type));
299 |
300 | return searchRequest;
301 | }
302 |
303 | /**
304 | * Converts the search response into a NamedList that the Solr Response Writer can use.
305 | *
306 | * @param request the ES RestRequest
307 | * @param response the ES SearchResponse
308 | * @return a NamedList of the response
309 | */
310 | private NamedList createSearchResponse(Map> params, RestRequest request, SearchResponse response) {
311 | NamedList resp = new SimpleOrderedMap();
312 | resp.add("responseHeader", createResponseHeader(params, request, response));
313 | resp.add("response", convertToSolrDocumentList(request, response));
314 |
315 | // add highlight node if highlighting was requested
316 | NamedList highlighting = createHighlightResponse(request, response);
317 | if (highlighting != null) {
318 | resp.add("highlighting", highlighting);
319 | }
320 |
321 | // add faceting node if faceting was requested
322 | NamedList faceting = createFacetResponse(request, response);
323 | if (faceting != null) {
324 | resp.add("facet_counts", faceting);
325 | }
326 |
327 | return resp;
328 | }
329 |
330 | /**
331 | * Creates the Solr response header based on the search response.
332 | *
333 | * @param request the ES RestRequest
334 | * @param response the ES SearchResponse
335 | * @return the response header as a NamedList
336 | */
337 | private NamedList createResponseHeader(Map> params, RestRequest request, SearchResponse response) {
338 | // generate response header
339 | NamedList responseHeader = new SimpleOrderedMap();
340 | responseHeader.add("status", 0);
341 | responseHeader.add("QTime", response.tookInMillis());
342 |
343 | // echo params in header
344 | NamedList solrParams = new SimpleOrderedMap();
345 | for (String param : params.keySet()) {
346 | List paramValue = params.get(param);
347 | if (paramValue != null && !paramValue.isEmpty()) {
348 | solrParams.add(param, paramValue.size() > 1 ? paramValue : paramValue.get(0));
349 | }
350 | }
351 |
352 | responseHeader.add("params", solrParams);
353 |
354 | return responseHeader;
355 | }
356 |
357 | /**
358 | * Converts the search results into a SolrDocumentList that can be serialized
359 | * by the Solr Response Writer.
360 | *
361 | * @param request the ES RestRequest
362 | * @param response the ES SearchResponse
363 | * @return search results as a SolrDocumentList
364 | */
365 | private SolrDocumentList convertToSolrDocumentList(RestRequest request, SearchResponse response) {
366 | SolrDocumentList results = new SolrDocumentList();
367 |
368 | // get the ES hits
369 | SearchHits hits = response.getHits();
370 |
371 | // set the result information on the SolrDocumentList
372 | results.setMaxScore(hits.getMaxScore());
373 | results.setNumFound(hits.getTotalHits());
374 | results.setStart(request.paramAsInt("start", 0));
375 |
376 | // loop though the results and convert each
377 | // one to a SolrDocument
378 | for (SearchHit hit : hits.getHits()) {
379 | SolrDocument doc = new SolrDocument();
380 |
381 | // always add score to document
382 | doc.addField("score", hit.score());
383 |
384 | // attempt to get the returned fields
385 | // if none returned, use the source fields
386 | Map fields = hit.getFields();
387 | Map source = hit.sourceAsMap();
388 | if (fields.isEmpty()) {
389 | if (source != null) {
390 | for (String sourceField : source.keySet()) {
391 | Object fieldValue = source.get(sourceField);
392 |
393 | // ES does not return date fields as Date Objects
394 | // detect if the string is a date, and if so
395 | // convert it to a Date object
396 | if (fieldValue.getClass() == String.class) {
397 | if (datePattern.matcher(fieldValue.toString()).matches()) {
398 | fieldValue = dateFormat.parseDateTime(fieldValue.toString()).toDate();
399 | }
400 | }
401 |
402 | doc.addField(sourceField, fieldValue);
403 | }
404 | }
405 | } else {
406 | for (String fieldName : fields.keySet()) {
407 | SearchHitField field = fields.get(fieldName);
408 | Object fieldValue = field.getValue();
409 |
410 | // ES does not return date fields as Date Objects
411 | // detect if the string is a date, and if so
412 | // convert it to a Date object
413 | if (fieldValue.getClass() == String.class) {
414 | if (datePattern.matcher(fieldValue.toString()).matches()) {
415 | fieldValue = dateFormat.parseDateTime(fieldValue.toString()).toDate();
416 | }
417 | }
418 |
419 | doc.addField(fieldName, fieldValue);
420 | }
421 | }
422 |
423 | // add the SolrDocument to the SolrDocumentList
424 | results.add(doc);
425 | }
426 |
427 | return results;
428 | }
429 |
430 | /**
431 | * Creates a NamedList for the for document highlighting response
432 | *
433 | * @param request the ES RestRequest
434 | * @param response the ES SearchResponse
435 | * @return a NamedList if highlighting was requested, null if not
436 | */
437 | private NamedList createHighlightResponse(RestRequest request, SearchResponse response) {
438 | NamedList highlightResponse = null;
439 |
440 | // if highlighting was requested create the NamedList for the highlights
441 | if (request.paramAsBoolean("hl", false)) {
442 | highlightResponse = new SimpleOrderedMap();
443 | SearchHits hits = response.getHits();
444 | // for each hit, get each highlight field and put the list
445 | // of highlight fragments in a NamedList specific to the hit
446 | for (SearchHit hit : hits.getHits()) {
447 | NamedList docHighlights = new SimpleOrderedMap();
448 | Map highlightFields = hit.getHighlightFields();
449 | for (String fieldName : highlightFields.keySet()) {
450 | HighlightField highlightField = highlightFields.get(fieldName);
451 | docHighlights.add(fieldName, highlightField.getFragments());
452 | }
453 |
454 | // highlighting by placing the doc highlights in the response
455 | // based on the document id
456 | highlightResponse.add(hit.field("id").getValue().toString(), docHighlights);
457 | }
458 | }
459 |
460 | // return the highlight response
461 | return highlightResponse;
462 | }
463 |
464 | private NamedList createFacetResponse(RestRequest request, SearchResponse response) {
465 | NamedList facetResponse = null;
466 |
467 | if (request.paramAsBoolean("facet", false)) {
468 | facetResponse = new SimpleOrderedMap();
469 |
470 | // create NamedLists for field and query facets
471 | NamedList termFacets = new SimpleOrderedMap();
472 | NamedList queryFacets = new SimpleOrderedMap();
473 |
474 | // loop though all the facets populating the NamedLists we just created
475 | Iterator facetIter = response.facets().iterator();
476 | while (facetIter.hasNext()) {
477 | Facet facet = facetIter.next();
478 | if (facet.type().equals(TermsFacet.TYPE)) {
479 | // we have term facet, create NamedList to store terms
480 | TermsFacet termFacet = (TermsFacet) facet;
481 | NamedList termFacetObj = new SimpleOrderedMap();
482 | for (TermsFacet.Entry tfEntry : termFacet.entries()) {
483 | termFacetObj.add(tfEntry.term(), tfEntry.count());
484 | }
485 |
486 | termFacets.add(facet.getName(), termFacetObj);
487 | } else if (facet.type().equals(QueryFacet.TYPE)) {
488 | QueryFacet queryFacet = (QueryFacet) facet;
489 | queryFacets.add(queryFacet.getName(), queryFacet.count());
490 | }
491 | }
492 |
493 | facetResponse.add("facet_fields", termFacets);
494 | facetResponse.add("facet_queries", queryFacets);
495 |
496 | // add dummy facet_dates and facet_ranges since we dont support them yet
497 | facetResponse.add("facet_dates", new SimpleOrderedMap());
498 | facetResponse.add("facet_ranges", new SimpleOrderedMap());
499 |
500 | }
501 |
502 | return facetResponse;
503 | }
504 | }
--------------------------------------------------------------------------------
/src/main/java/co/diji/rest/SolrUpdateHandlerRestAction.java:
--------------------------------------------------------------------------------
1 | package co.diji.rest;
2 |
3 | import java.io.ByteArrayInputStream;
4 | import java.io.IOException;
5 | import java.io.StringReader;
6 | import java.security.MessageDigest;
7 | import java.security.NoSuchAlgorithmException;
8 | import java.util.ArrayList;
9 | import java.util.Collection;
10 | import java.util.HashMap;
11 | import java.util.List;
12 | import java.util.Map;
13 | import java.util.UUID;
14 |
15 | import javax.xml.stream.XMLInputFactory;
16 | import javax.xml.stream.XMLStreamConstants;
17 | import javax.xml.stream.XMLStreamException;
18 | import javax.xml.stream.XMLStreamReader;
19 |
20 | import org.apache.commons.codec.binary.Hex;
21 | import org.apache.solr.client.solrj.request.JavaBinUpdateRequestCodec;
22 | import org.apache.solr.client.solrj.request.UpdateRequest;
23 | import org.apache.solr.common.SolrInputDocument;
24 | import org.apache.solr.common.SolrInputField;
25 | import org.apache.solr.common.util.NamedList;
26 | import org.apache.solr.common.util.SimpleOrderedMap;
27 | import org.elasticsearch.action.ActionListener;
28 | import org.elasticsearch.action.WriteConsistencyLevel;
29 | import org.elasticsearch.action.bulk.BulkItemResponse;
30 | import org.elasticsearch.action.bulk.BulkRequest;
31 | import org.elasticsearch.action.bulk.BulkResponse;
32 | import org.elasticsearch.action.delete.DeleteRequest;
33 | import org.elasticsearch.action.index.IndexRequest;
34 | import org.elasticsearch.action.support.replication.ReplicationType;
35 | import org.elasticsearch.client.Client;
36 | import org.elasticsearch.client.Requests;
37 | import org.elasticsearch.common.inject.Inject;
38 | import org.elasticsearch.common.settings.Settings;
39 | import org.elasticsearch.plugin.diji.MockSolrPlugin;
40 | import org.elasticsearch.rest.BaseRestHandler;
41 | import org.elasticsearch.rest.RestChannel;
42 | import org.elasticsearch.rest.RestController;
43 | import org.elasticsearch.rest.RestRequest;
44 | import org.elasticsearch.rest.XContentThrowableRestResponse;
45 |
46 | import co.diji.solr.SolrResponseWriter;
47 |
48 | public class SolrUpdateHandlerRestAction extends BaseRestHandler {
49 |
50 | // content types
51 | private final String contentTypeFormEncoded = "application/x-www-form-urlencoded";
52 |
53 | // fields in the Solr input document to scan for a document id
54 | private final String[] idFields = {"id", "docid", "documentid", "contentid", "uuid", "url"};
55 |
56 | // the xml input factory
57 | private final XMLInputFactory inputFactory = XMLInputFactory.newInstance();
58 |
59 | // the response writer
60 | private final SolrResponseWriter solrResponseWriter = new SolrResponseWriter();
61 |
62 | // Set this flag to false if you want to disable the hashing of id's as they are provided by the Solr Input document
63 | // , which is the default behaviour.
64 | // You can configure this by adding 'plugin.diji.MockSolrPlugin.hashIds: false' to elasticsearch.yml
65 | private final boolean hashIds;
66 |
67 | /**
68 | * Rest actions that mock Solr update handlers
69 | *
70 | * @param settings ES settings
71 | * @param client ES client
72 | * @param restController ES rest controller
73 | */
74 | @Inject
75 | public SolrUpdateHandlerRestAction(Settings settings, Client client, RestController restController) {
76 | super(settings, client);
77 |
78 | hashIds = settings.getComponentSettings(MockSolrPlugin.class).getAsBoolean("MockSolrPlugin.hashIds", true);
79 | logger.info("Solr input document id's will " + (hashIds ? "" : "not ") + "be hashed to created ElasticSearch document id's");
80 |
81 | // register update handlers
82 | // specifying and index and type is optional
83 | restController.registerHandler(RestRequest.Method.POST, "/_solr/update", this);
84 | restController.registerHandler(RestRequest.Method.POST, "/_solr/update/{handler}", this);
85 | restController.registerHandler(RestRequest.Method.POST, "/{index}/_solr/update", this);
86 | restController.registerHandler(RestRequest.Method.POST, "/{index}/_solr/update/{handler}", this);
87 | restController.registerHandler(RestRequest.Method.POST, "/{index}/{type}/_solr/update", this);
88 | restController.registerHandler(RestRequest.Method.POST, "/{index}/{type}/_solr/update/{handler}", this);
89 | }
90 |
91 | /*
92 | * (non-Javadoc)
93 | * @see org.elasticsearch.rest.RestHandler#handleRequest(org.elasticsearch.rest.RestRequest, org.elasticsearch.rest.RestChannel)
94 | */
95 | public void handleRequest(final RestRequest request, final RestChannel channel) {
96 | // Solr will send commits/optimize as encoded form parameters
97 | // detect this and just send the response without processing
98 | // we don't need to do commits with ES
99 | // TODO: support optimize
100 | if (request.header("Content-Type").contains(contentTypeFormEncoded)) {
101 | // find the output writer specified
102 | // it will be inside the content since we have form encoded
103 | // parameters
104 | String qstr = request.content().toUtf8();
105 | Map params = request.params();
106 | if (params.containsKey("wt")) {
107 | // output writer already found
108 | } else if (qstr.contains("wt=javabin")) {
109 | params.put("wt", "javabin");
110 | } else if (qstr.contains("wt=xml")) {
111 | params.put("wt", "xml");
112 | } else {
113 | // we have an output writer we don't support yet
114 | // put junk into wt so sendResponse detects unknown wt
115 | logger.warn("Unknown wt for commit/optimize");
116 | params.put("wt", "invalid");
117 | }
118 |
119 | // send response to Solr
120 | sendResponse(request, channel);
121 | return;
122 | }
123 |
124 | // get the type of Solr update handler we want to mock, default to xml
125 | final String handler = request.hasParam("handler") ? request.param("handler").toLowerCase() : "xml";
126 |
127 | // Requests are typically sent to Solr in batches of documents
128 | // We can copy that by submitting batch requests to Solr
129 | BulkRequest bulkRequest = Requests.bulkRequest();
130 |
131 | // parse and handle the content
132 | if (handler.equals("xml")) {
133 | // XML Content
134 | try {
135 | // create parser for the content
136 | XMLStreamReader parser = inputFactory.createXMLStreamReader(new StringReader(request.content().toUtf8()));
137 |
138 | // parse the xml
139 | // we only care about doc and delete tags for now
140 | boolean stop = false;
141 | while (!stop) {
142 | // get the xml "event"
143 | int event = parser.next();
144 | switch (event) {
145 | case XMLStreamConstants.END_DOCUMENT :
146 | // this is the end of the document
147 | // close parser and exit while loop
148 | parser.close();
149 | stop = true;
150 | break;
151 | case XMLStreamConstants.START_ELEMENT :
152 | // start of an xml tag
153 | // determine if we need to add or delete a document
154 | String currTag = parser.getLocalName();
155 | if ("doc".equals(currTag)) {
156 | // add a document
157 | Map doc = parseXmlDoc(parser);
158 | if (doc != null) {
159 | bulkRequest.add(getIndexRequest(doc, request));
160 | }
161 | } else if ("delete".equals(currTag)) {
162 | // delete a document
163 | String docid = parseXmlDelete(parser);
164 | if (docid != null) {
165 | bulkRequest.add(getDeleteRequest(docid, request));
166 | }
167 | }
168 | break;
169 | }
170 | }
171 | } catch (Exception e) {
172 | // some sort of error processing the xml input
173 | try {
174 | logger.error("Error processing xml input", e);
175 | channel.sendResponse(new XContentThrowableRestResponse(request, e));
176 | } catch (IOException e1) {
177 | logger.error("Failed to send error response", e1);
178 | }
179 | }
180 | } else if (handler.equals("javabin")) {
181 | // JavaBin Content
182 | try {
183 | // We will use the JavaBin codec from solrj
184 | // unmarshal the input to a SolrUpdate request
185 | JavaBinUpdateRequestCodec codec = new JavaBinUpdateRequestCodec();
186 | UpdateRequest req = codec.unmarshal(new ByteArrayInputStream(request.content().array()), null);
187 |
188 | // Get the list of documents to index out of the UpdateRequest
189 | // Add each document to the bulk request
190 | // convert the SolrInputDocument into a map which will be used as the ES source field
191 | List docs = req.getDocuments();
192 | if (docs != null) {
193 | for (SolrInputDocument doc : docs) {
194 | bulkRequest.add(getIndexRequest(convertToMap(doc), request));
195 | }
196 | }
197 |
198 | // See if we have any documents to delete
199 | // if yes, add them to the bulk request
200 | if (req.getDeleteById() != null) {
201 | for (String id : req.getDeleteById()) {
202 | bulkRequest.add(getDeleteRequest(id, request));
203 | }
204 | }
205 | } catch (Exception e) {
206 | // some sort of error processing the javabin input
207 | try {
208 | logger.error("Error processing javabin input", e);
209 | channel.sendResponse(new XContentThrowableRestResponse(request, e));
210 | } catch (IOException e1) {
211 | logger.error("Failed to send error response", e1);
212 | }
213 | }
214 | }
215 |
216 | // only submit the bulk request if there are index/delete actions
217 | // it is possible not to have any actions when parsing xml due to the
218 | // commit and optimize messages that will not generate documents
219 | if (bulkRequest.numberOfActions() > 0) {
220 | client.bulk(bulkRequest, new ActionListener() {
221 |
222 | // successful bulk request
223 | public void onResponse(BulkResponse response) {
224 | logger.info("Bulk request completed");
225 | for (BulkItemResponse itemResponse : response) {
226 | if (itemResponse.failed()) {
227 | logger.error("Index request failed {index:{}, type:{}, id:{}, reason:{}}",
228 | itemResponse.index(),
229 | itemResponse.type(),
230 | itemResponse.id(),
231 | itemResponse.failure().message());
232 | }
233 | }
234 | }
235 |
236 | // failed bulk request
237 | public void onFailure(Throwable e) {
238 | logger.error("Bulk request failed", e);
239 | }
240 | });
241 | }
242 |
243 | // send dummy response to Solr so the clients don't choke
244 | sendResponse(request, channel);
245 | }
246 |
247 | /**
248 | * Sends a dummy response to the Solr client
249 | *
250 | * @param request ES rest request
251 | * @param channel ES rest channel
252 | */
253 | private void sendResponse(RestRequest request, RestChannel channel) {
254 | // create NamedList with dummy Solr response
255 | NamedList solrResponse = new SimpleOrderedMap();
256 | NamedList responseHeader = new SimpleOrderedMap();
257 | responseHeader.add("status", 0);
258 | responseHeader.add("QTime", 5);
259 | solrResponse.add("responseHeader", responseHeader);
260 |
261 | // send the dummy response
262 | solrResponseWriter.writeResponse(solrResponse, request, channel);
263 | }
264 |
265 | /**
266 | * Generates an ES DeleteRequest object based on the Solr document id
267 | *
268 | * @param id the Solr document id
269 | * @param request the ES rest request
270 | * @return the ES delete request
271 | */
272 | private DeleteRequest getDeleteRequest(String id, RestRequest request) {
273 |
274 | // get the index and type we want to execute this delete request on
275 | final String index = request.hasParam("index") ? request.param("index") : "solr";
276 | final String type = request.hasParam("type") ? request.param("type") : "docs";
277 |
278 | // create the delete request object
279 | DeleteRequest deleteRequest = new DeleteRequest(index, type, getId(id));
280 | deleteRequest.parent(request.param("parent"));
281 |
282 | // TODO: this was causing issues, do we need it?
283 | // deleteRequest.version(RestActions.parseVersion(request));
284 | // deleteRequest.versionType(VersionType.fromString(request.param("version_type"),
285 | // deleteRequest.versionType()));
286 |
287 | deleteRequest.routing(request.param("routing"));
288 |
289 | return deleteRequest;
290 | }
291 |
292 | /**
293 | * Converts a SolrInputDocument into an ES IndexRequest
294 | *
295 | * @param doc the Solr input document to convert
296 | * @param request the ES rest request
297 | * @return the ES index request object
298 | */
299 | private IndexRequest getIndexRequest(Map doc, RestRequest request) {
300 | // get the index and type we want to index the document in
301 | final String index = request.hasParam("index") ? request.param("index") : "solr";
302 | final String type = request.hasParam("type") ? request.param("type") : "docs";
303 |
304 | // Get the id from request or if not available generate an id for the document
305 | String id = request.hasParam("id") ? request.param("id") : getIdForDoc(doc);
306 |
307 | // create an IndexRequest for this document
308 | IndexRequest indexRequest = new IndexRequest(index, type, id);
309 | indexRequest.routing(request.param("routing"));
310 | indexRequest.parent(request.param("parent"));
311 | indexRequest.source(doc);
312 | indexRequest.timeout(request.paramAsTime("timeout", IndexRequest.DEFAULT_TIMEOUT));
313 | indexRequest.refresh(request.paramAsBoolean("refresh", indexRequest.refresh()));
314 |
315 | // TODO: this caused issues, do we need it?
316 | // indexRequest.version(RestActions.parseVersion(request));
317 | // indexRequest.versionType(VersionType.fromString(request.param("version_type"),
318 | // indexRequest.versionType()));
319 |
320 | indexRequest.percolate(request.param("percolate", null));
321 | indexRequest.opType(IndexRequest.OpType.INDEX);
322 |
323 | // TODO: force creation of index, do we need it?
324 | // indexRequest.create(true);
325 |
326 | String replicationType = request.param("replication");
327 | if (replicationType != null) {
328 | indexRequest.replicationType(ReplicationType.fromString(replicationType));
329 | }
330 |
331 | String consistencyLevel = request.param("consistency");
332 | if (consistencyLevel != null) {
333 | indexRequest.consistencyLevel(WriteConsistencyLevel.fromString(consistencyLevel));
334 | }
335 |
336 | // we just send a response, no need to fork
337 | indexRequest.listenerThreaded(true);
338 |
339 | // we don't spawn, then fork if local
340 | indexRequest.operationThreaded(true);
341 |
342 | return indexRequest;
343 | }
344 |
345 | /**
346 | * Generates document id. A Solr document id may not be a valid ES id, so we attempt to find the Solr document id and convert it
347 | * into a valid ES document id. We keep the original Solr id so the document can be found and deleted later if needed.
348 | *
349 | * We check for Solr document id's in the following fields: id, docid, documentid, contentid, uuid, url
350 | *
351 | * If no id is found, we generate a random one.
352 | *
353 | * @param doc the input document
354 | * @return the generated document id
355 | */
356 | private String getIdForDoc(Map doc) {
357 | // start with a random id
358 | String id = UUID.randomUUID().toString();
359 |
360 | // scan the input document for an id
361 | for (String idField : idFields) {
362 | if (doc.containsKey(idField)) {
363 | id = doc.get(idField).toString();
364 | break;
365 | }
366 | }
367 |
368 | // always store the id back into the "id" field
369 | // so we can get it back in results
370 | doc.put("id", id);
371 |
372 | // return the id which is the md5 of either the
373 | // random uuid or id found in the input document.
374 | return getId(id);
375 | }
376 |
377 | /**
378 | * Return the given id or a hashed version thereof, based on the plugin configuration
379 | *
380 | * @param id
381 | * @return
382 | */
383 |
384 | private final String getId(String id) {
385 | return hashIds ? getMD5(id) : id;
386 | }
387 |
388 | /**
389 | * Calculates the md5 hex digest of the given input string
390 | *
391 | * @param input the string to md5
392 | * @return the md5 hex digest
393 | */
394 | private String getMD5(String input) {
395 | String id = "";
396 | MessageDigest md;
397 | try {
398 | md = MessageDigest.getInstance("MD5");
399 | id = new String(Hex.encodeHex(md.digest(input.getBytes())));
400 | } catch (NoSuchAlgorithmException e) {
401 | id = input;
402 | }
403 |
404 | return id;
405 | }
406 |
407 | /**
408 | * Converts a SolrInputDocument into a Map
409 | *
410 | * @param doc the SolrInputDocument to convert
411 | * @return the input document as a map
412 | */
413 | private Map convertToMap(SolrInputDocument doc) {
414 | // create the Map we will put the fields in
415 | Map newDoc = new HashMap();
416 |
417 | // loop though all the fields and insert them into the map
418 | Collection fields = doc.values();
419 | if (fields != null) {
420 | for (SolrInputField field : fields) {
421 | newDoc.put(field.getName(), field.getValue());
422 | }
423 | }
424 |
425 | return newDoc;
426 | }
427 |
428 | /**
429 | * Reads a SolrXML document into a map of fields
430 | *
431 | * @param parser the xml parser
432 | * @return the document as a map
433 | * @throws XMLStreamException
434 | */
435 | private Map parseXmlDoc(XMLStreamReader parser) throws XMLStreamException {
436 | Map doc = new HashMap();
437 | StringBuilder buf = new StringBuilder();
438 | String name = null;
439 | boolean stop = false;
440 | // infinite loop until we are done parsing the document or an error occurs
441 | while (!stop) {
442 | int event = parser.next();
443 | switch (event) {
444 | case XMLStreamConstants.START_ELEMENT :
445 | buf.setLength(0);
446 | String localName = parser.getLocalName();
447 | // we are looking for field elements only
448 | if (!"field".equals(localName)) {
449 | logger.warn("unexpected xml tag /doc/" + localName);
450 | doc = null;
451 | stop = true;
452 | }
453 |
454 | // get the name attribute of the field
455 | String attrName = "";
456 | String attrVal = "";
457 | for (int i = 0; i < parser.getAttributeCount(); i++) {
458 | attrName = parser.getAttributeLocalName(i);
459 | attrVal = parser.getAttributeValue(i);
460 | if ("name".equals(attrName)) {
461 | name = attrVal;
462 | }
463 | }
464 | break;
465 | case XMLStreamConstants.END_ELEMENT :
466 | if ("doc".equals(parser.getLocalName())) {
467 | // we are done parsing the doc
468 | // break out of loop
469 | stop = true;
470 | } else if ("field".equals(parser.getLocalName())) {
471 | // put the field value into the map
472 | // handle multiple values by putting them into a list
473 | if (doc.containsKey(name) && (doc.get(name) instanceof List)) {
474 | List vals = (List) doc.get(name);
475 | vals.add(buf.toString());
476 | doc.put(name, vals);
477 | } else if (doc.containsKey(name)) {
478 | List vals = new ArrayList();
479 | vals.add((String) doc.get(name));
480 | vals.add(buf.toString());
481 | doc.put(name, vals);
482 | } else {
483 | doc.put(name, buf.toString());
484 | }
485 | }
486 | break;
487 | case XMLStreamConstants.SPACE :
488 | case XMLStreamConstants.CDATA :
489 | case XMLStreamConstants.CHARACTERS :
490 | // save all text data
491 | buf.append(parser.getText());
492 | break;
493 | }
494 | }
495 |
496 | // return the parsed doc
497 | return doc;
498 | }
499 |
500 | /**
501 | * Parse the document id out of the SolrXML delete command
502 | *
503 | * @param parser the xml parser
504 | * @return the document id to delete
505 | * @throws XMLStreamException
506 | */
507 | private String parseXmlDelete(XMLStreamReader parser) throws XMLStreamException {
508 | String docid = null;
509 | StringBuilder buf = new StringBuilder();
510 | boolean stop = false;
511 | // infinite loop until we get docid or error
512 | while (!stop) {
513 | int event = parser.next();
514 | switch (event) {
515 | case XMLStreamConstants.START_ELEMENT :
516 | // we just want the id node
517 | String mode = parser.getLocalName();
518 | if (!"id".equals(mode)) {
519 | logger.warn("unexpected xml tag /delete/" + mode);
520 | stop = true;
521 | }
522 | buf.setLength(0);
523 | break;
524 | case XMLStreamConstants.END_ELEMENT :
525 | String currTag = parser.getLocalName();
526 | if ("id".equals(currTag)) {
527 | // we found the id
528 | docid = buf.toString();
529 | } else if ("delete".equals(currTag)) {
530 | // done parsing, exit loop
531 | stop = true;
532 | } else {
533 | logger.warn("unexpected xml tag /delete/" + currTag);
534 | }
535 | break;
536 | case XMLStreamConstants.SPACE :
537 | case XMLStreamConstants.CDATA :
538 | case XMLStreamConstants.CHARACTERS :
539 | // save all text data (this is the id)
540 | buf.append(parser.getText());
541 | break;
542 | }
543 | }
544 |
545 | // return the extracted docid
546 | return docid;
547 | }
548 | }
549 |
--------------------------------------------------------------------------------
/src/main/java/co/diji/solr/SolrResponseWriter.java:
--------------------------------------------------------------------------------
1 | package co.diji.solr;
2 |
3 | import java.io.ByteArrayOutputStream;
4 | import java.io.IOException;
5 | import java.io.StringWriter;
6 | import java.io.Writer;
7 |
8 | import org.apache.solr.common.util.JavaBinCodec;
9 | import org.apache.solr.common.util.NamedList;
10 | import org.elasticsearch.common.logging.ESLogger;
11 | import org.elasticsearch.common.logging.Loggers;
12 | import org.elasticsearch.rest.BytesRestResponse;
13 | import org.elasticsearch.rest.RestChannel;
14 | import org.elasticsearch.rest.RestRequest;
15 |
16 | /**
17 | * Class to handle sending responses to Solr clients.
18 | * Supports xml and javabin formats.
19 | *
20 | */
21 | public class SolrResponseWriter {
22 | protected final ESLogger logger;
23 |
24 | private final String contentTypeOctet = "application/octet-stream";
25 | private final String contentTypeXml = "application/xml; charset=UTF-8";
26 |
27 | public SolrResponseWriter() {
28 | this.logger = Loggers.getLogger(SolrResponseWriter.class);
29 | }
30 |
31 | /**
32 | * Serializes the NamedList in the specified output format and sends it to the Solr Client.
33 | *
34 | * @param obj the NamedList response to serialize
35 | * @param request the ES RestRequest
36 | * @param channel the ES RestChannel
37 | */
38 | public void writeResponse(NamedList obj, RestRequest request, RestChannel channel) {
39 | // determine what kind of output writer the Solr client is expecting
40 | final String wt = request.hasParam("wt") ? request.param("wt").toLowerCase() : "xml";
41 |
42 | // determine what kind of response we need to send
43 | if (wt.equals("xml")) {
44 | writeXmlResponse(obj, channel);
45 | } else if (wt.equals("javabin")) {
46 | writeJavaBinResponse(obj, channel);
47 | }
48 | }
49 |
50 | /**
51 | * Write the response object in JavaBin format.
52 | *
53 | * @param obj the response object
54 | * @param channel the ES RestChannel
55 | */
56 | private void writeJavaBinResponse(NamedList obj, RestChannel channel) {
57 | ByteArrayOutputStream bo = new ByteArrayOutputStream();
58 |
59 | // try to marshal the data
60 | try {
61 | new JavaBinCodec().marshal(obj, bo);
62 | } catch (IOException e) {
63 | logger.error("Error writing JavaBin response", e);
64 | }
65 |
66 | // send the response
67 | channel.sendResponse(new BytesRestResponse(bo.toByteArray(), contentTypeOctet));
68 | }
69 |
70 | private void writeXmlResponse(NamedList obj, RestChannel channel) {
71 | Writer writer = new StringWriter();
72 |
73 | // try to serialize the data to xml
74 | try {
75 | writer.write(XMLWriter.XML_START1);
76 | writer.write(XMLWriter.XML_START2_NOSCHEMA);
77 |
78 | // initialize the xml writer
79 | XMLWriter xw = new XMLWriter(writer);
80 |
81 | // loop though each object and convert it to xml
82 | int sz = obj.size();
83 | for (int i = 0; i < sz; i++) {
84 | xw.writeVal(obj.getName(i), obj.getVal(i));
85 | }
86 |
87 | writer.write("\n\n");
88 | writer.close();
89 | } catch (IOException e) {
90 | logger.error("Error writing XML response", e);
91 | }
92 |
93 | // send the response
94 | channel.sendResponse(new BytesRestResponse(writer.toString().getBytes(), contentTypeXml));
95 | }
96 | }
--------------------------------------------------------------------------------
/src/main/java/co/diji/solr/XMLWriter.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Licensed to the Apache Software Foundation (ASF) under one or more
3 | * contributor license agreements. See the NOTICE file distributed with
4 | * this work for additional information regarding copyright ownership.
5 | * The ASF licenses this file to You under the Apache License, Version 2.0
6 | * (the "License"); you may not use this file except in compliance with
7 | * the License. You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | package co.diji.solr;
19 |
20 | import org.apache.solr.common.SolrDocument;
21 | import org.apache.solr.common.SolrDocumentList;
22 | import org.apache.solr.common.util.NamedList;
23 | import org.apache.solr.common.util.XML;
24 |
25 | import java.io.Writer;
26 | import java.io.IOException;
27 | import java.util.*;
28 |
29 | import org.apache.lucene.document.Fieldable;
30 | import org.elasticsearch.common.joda.time.DateTime;
31 | import org.elasticsearch.common.joda.time.format.DateTimeFormat;
32 | import org.elasticsearch.common.joda.time.format.DateTimeFormatter;
33 |
34 | /**
35 | * Writes objects to xml. This class is taken directly out of the
36 | * Solr source code and modified to remove the stuff we do not need
37 | * for the plugin.
38 | *
39 | */
40 | final public class XMLWriter {
41 |
42 | //
43 | // static thread safe part
44 | //
45 | public static final char[] XML_START1="\n".toCharArray();
46 | public static final char[] XML_START2_NOSCHEMA=(
47 | "\n"
48 | ).toCharArray();
49 |
50 | ////////////////////////////////////////////////////////////
51 | // request instance specific (non-static, not shared between threads)
52 | ////////////////////////////////////////////////////////////
53 |
54 | private final Writer writer;
55 |
56 | private final DateTimeFormatter dateFormat = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
57 |
58 | public XMLWriter(Writer writer) {
59 | this.writer = writer;
60 | }
61 |
62 | /** Writes the XML attribute name/val. A null val means that the attribute is missing. */
63 | public void writeAttr(String name, String val) throws IOException {
64 | writeAttr(name, val, true);
65 | }
66 |
67 | public void writeAttr(String name, String val, boolean escape) throws IOException{
68 | if (val != null) {
69 | writer.write(' ');
70 | writer.write(name);
71 | writer.write("=\"");
72 | if(escape){
73 | XML.escapeAttributeValue(val, writer);
74 | } else {
75 | writer.write(val);
76 | }
77 | writer.write('"');
78 | }
79 | }
80 |
81 | /**Writes a tag with attributes
82 | *
83 | * @param tag
84 | * @param attributes
85 | * @param closeTag
86 | * @param escape
87 | * @throws IOException
88 | */
89 | public void startTag(String tag, Map attributes, boolean closeTag, boolean escape) throws IOException {
90 | writer.write('<');
91 | writer.write(tag);
92 | if(!attributes.isEmpty()) {
93 | for (Map.Entry entry : attributes.entrySet()) {
94 | writeAttr(entry.getKey(), entry.getValue(), escape);
95 | }
96 | }
97 | if (closeTag) {
98 | writer.write("/>");
99 | } else {
100 | writer.write('>');
101 | }
102 | }
103 |
104 | /**Write a complete tag w/ attributes and cdata (the cdata is not enclosed in $lt;!CDATA[]!>
105 | * @param tag
106 | * @param attributes
107 | * @param cdata
108 | * @param escapeCdata
109 | * @param escapeAttr
110 | * @throws IOException
111 | */
112 | public void writeCdataTag(String tag, Map attributes, String cdata, boolean escapeCdata, boolean escapeAttr) throws IOException {
113 | writer.write('<');
114 | writer.write(tag);
115 | if (!attributes.isEmpty()) {
116 | for (Map.Entry entry : attributes.entrySet()) {
117 | writeAttr(entry.getKey(), entry.getValue(), escapeAttr);
118 | }
119 | }
120 | writer.write('>');
121 | if (cdata != null && cdata.length() > 0) {
122 | if (escapeCdata) {
123 | XML.escapeCharData(cdata, writer);
124 | } else {
125 | writer.write(cdata, 0, cdata.length());
126 | }
127 | }
128 | writer.write("");
129 | writer.write(tag);
130 | writer.write('>');
131 | }
132 |
133 |
134 |
135 | public void startTag(String tag, String name, boolean closeTag) throws IOException {
136 | writer.write('<');
137 | writer.write(tag);
138 | if (name!=null) {
139 | writeAttr("name", name);
140 | if (closeTag) {
141 | writer.write("/>");
142 | } else {
143 | writer.write(">");
144 | }
145 | } else {
146 | if (closeTag) {
147 | writer.write("/>");
148 | } else {
149 | writer.write('>');
150 | }
151 | }
152 | }
153 |
154 | private static final Comparator fieldnameComparator = new Comparator() {
155 | public int compare(Object o, Object o1) {
156 | Fieldable f1 = (Fieldable)o; Fieldable f2 = (Fieldable)o1;
157 | int cmp = f1.name().compareTo(f2.name());
158 | return cmp;
159 | // note - the sort is stable, so this should not have affected the ordering
160 | // of fields with the same name w.r.t eachother.
161 | }
162 | };
163 |
164 |
165 | /**
166 | * @since solr 1.3
167 | */
168 | final void writeDoc(String name, SolrDocument doc, Set returnFields, boolean includeScore) throws IOException {
169 | startTag("doc", name, false);
170 |
171 | if (includeScore && returnFields != null ) {
172 | returnFields.add( "score" );
173 | }
174 |
175 | for (String fname : doc.getFieldNames()) {
176 | if (returnFields!=null && !returnFields.contains(fname)) {
177 | continue;
178 | }
179 | Object val = doc.getFieldValue(fname);
180 |
181 | writeVal(fname, val);
182 | }
183 |
184 | writer.write("");
185 | }
186 |
187 |
188 | private static interface DocumentListInfo {
189 | Float getMaxScore();
190 | int getCount();
191 | long getNumFound();
192 | long getStart();
193 | void writeDocs( boolean includeScore, Set fields ) throws IOException;
194 | }
195 |
196 | private final void writeDocuments(
197 | String name,
198 | DocumentListInfo docs,
199 | Set fields) throws IOException
200 | {
201 | boolean includeScore=false;
202 | if (fields!=null) {
203 | includeScore = fields.contains("score");
204 | if (fields.size()==0 || (fields.size()==1 && includeScore) || fields.contains("*")) {
205 | fields=null; // null means return all stored fields
206 | }
207 | }
208 |
209 | int sz=docs.getCount();
210 |
211 | writer.write(" ");
220 | return;
221 | } else {
222 | writer.write('>');
223 | }
224 |
225 | docs.writeDocs(includeScore, fields);
226 |
227 | writer.write("");
228 | }
229 |
230 | public final void writeSolrDocumentList(String name, final SolrDocumentList docs, Set fields) throws IOException
231 | {
232 | this.writeDocuments( name, new DocumentListInfo()
233 | {
234 | public int getCount() {
235 | return docs.size();
236 | }
237 |
238 | public Float getMaxScore() {
239 | return docs.getMaxScore();
240 | }
241 |
242 | public long getNumFound() {
243 | return docs.getNumFound();
244 | }
245 |
246 | public long getStart() {
247 | return docs.getStart();
248 | }
249 |
250 | public void writeDocs(boolean includeScore, Set fields) throws IOException {
251 | for( SolrDocument doc : docs ) {
252 | writeDoc(null, doc, fields, includeScore);
253 | }
254 | }
255 | }, fields );
256 | }
257 |
258 | public void writeVal(String name, Object val) throws IOException {
259 |
260 | // if there get to be enough types, perhaps hashing on the type
261 | // to get a handler might be faster (but types must be exact to do that...)
262 |
263 | // go in order of most common to least common
264 | if (val==null) {
265 | writeNull(name);
266 | } else if (val instanceof String) {
267 | writeStr(name, (String)val);
268 | } else if (val instanceof Integer) {
269 | // it would be slower to pass the int ((Integer)val).intValue()
270 | writeInt(name, val.toString());
271 | } else if (val instanceof Boolean) {
272 | // could be optimized... only two vals
273 | writeBool(name, val.toString());
274 | } else if (val instanceof Long) {
275 | writeLong(name, val.toString());
276 | } else if (val instanceof Date) {
277 | writeDate(name,(Date)val);
278 | } else if (val instanceof Float) {
279 | // we pass the float instead of using toString() because
280 | // it may need special formatting. same for double.
281 | writeFloat(name, ((Float)val).floatValue());
282 | } else if (val instanceof Double) {
283 | writeDouble(name, ((Double)val).doubleValue());
284 | } else if (val instanceof SolrDocumentList) {
285 | // requires access to IndexReader
286 | writeSolrDocumentList(name, (SolrDocumentList)val, null);
287 | }else if (val instanceof Map) {
288 | writeMap(name, (Map)val);
289 | } else if (val instanceof NamedList) {
290 | writeNamedList(name, (NamedList)val);
291 | } else if (val instanceof Iterable) {
292 | writeArray(name,((Iterable)val).iterator());
293 | } else if (val instanceof Object[]) {
294 | writeArray(name,(Object[])val);
295 | } else if (val instanceof Iterator) {
296 | writeArray(name,(Iterator)val);
297 | } else {
298 | // default...
299 | writeStr(name, val.getClass().getName() + ':' + val.toString());
300 | }
301 | }
302 |
303 | //
304 | // Generic compound types
305 | //
306 |
307 | public void writeNamedList(String name, NamedList val) throws IOException {
308 | int sz = val.size();
309 | startTag("lst", name, sz<=0);
310 |
311 | for (int i=0; i 0) {
316 | writer.write("");
317 | }
318 | }
319 |
320 |
321 | /**
322 | * writes a Map in the same format as a NamedList, using the
323 | * stringification of the key Object when it's non-null.
324 | *
325 | * @param name
326 | * @param map
327 | * @throws IOException
328 | * @see SolrQueryResponse Note on Returnable Data
329 | */
330 | public void writeMap(String name, Map map) throws IOException {
331 | int sz = map.size();
332 | startTag("lst", name, sz<=0);
333 |
334 | for (Map.Entry entry : map.entrySet()) {
335 | Object k = entry.getKey();
336 | Object v = entry.getValue();
337 | // if (sz 0) {
342 | writer.write("");
343 | }
344 | }
345 |
346 | public void writeArray(String name, Object[] val) throws IOException {
347 | writeArray(name, Arrays.asList(val).iterator());
348 | }
349 |
350 | public void writeArray(String name, Iterator iter) throws IOException {
351 | if( iter.hasNext() ) {
352 | startTag("arr", name, false );
353 |
354 | while( iter.hasNext() ) {
355 | writeVal(null, iter.next());
356 | }
357 |
358 | writer.write("");
359 | }
360 | else {
361 | startTag("arr", name, true );
362 | }
363 | }
364 |
365 | //
366 | // Primitive types
367 | //
368 |
369 | public void writeNull(String name) throws IOException {
370 | writePrim("null",name,"",false);
371 | }
372 |
373 | public void writeStr(String name, String val) throws IOException {
374 | writePrim("str",name,val,true);
375 | }
376 |
377 | public void writeInt(String name, String val) throws IOException {
378 | writePrim("int",name,val,false);
379 | }
380 |
381 | public void writeInt(String name, int val) throws IOException {
382 | writeInt(name,Integer.toString(val));
383 | }
384 |
385 | public void writeLong(String name, String val) throws IOException {
386 | writePrim("long",name,val,false);
387 | }
388 |
389 | public void writeLong(String name, long val) throws IOException {
390 | writeLong(name,Long.toString(val));
391 | }
392 |
393 | public void writeBool(String name, String val) throws IOException {
394 | writePrim("bool",name,val,false);
395 | }
396 |
397 | public void writeBool(String name, boolean val) throws IOException {
398 | writeBool(name,Boolean.toString(val));
399 | }
400 |
401 | public void writeShort(String name, String val) throws IOException {
402 | writePrim("short",name,val,false);
403 | }
404 |
405 | public void writeShort(String name, short val) throws IOException {
406 | writeInt(name,Short.toString(val));
407 | }
408 |
409 |
410 | public void writeByte(String name, String val) throws IOException {
411 | writePrim("byte",name,val,false);
412 | }
413 |
414 | public void writeByte(String name, byte val) throws IOException {
415 | writeInt(name,Byte.toString(val));
416 | }
417 |
418 |
419 | public void writeFloat(String name, String val) throws IOException {
420 | writePrim("float",name,val,false);
421 | }
422 |
423 | public void writeFloat(String name, float val) throws IOException {
424 | writeFloat(name,Float.toString(val));
425 | }
426 |
427 | public void writeDouble(String name, String val) throws IOException {
428 | writePrim("double",name,val,false);
429 | }
430 |
431 | public void writeDouble(String name, double val) throws IOException {
432 | writeDouble(name,Double.toString(val));
433 | }
434 |
435 | public void writeDate(String name, Date val) throws IOException {
436 | // updated to use Joda time
437 | writeDate(name, new DateTime(val).toString(dateFormat));
438 | }
439 |
440 | public void writeDate(String name, String val) throws IOException {
441 | writePrim("date",name,val,false);
442 | }
443 |
444 |
445 | //
446 | // OPT - specific writeInt, writeFloat, methods might be faster since
447 | // there would be less write calls (write(")
448 | //
449 | public void writePrim(String tag, String name, String val, boolean escape) throws IOException {
450 | // OPT - we could use a temp char[] (or a StringBuilder) and if the
451 | // size was small enough to fit (if escape==false we can calc exact size)
452 | // then we could put things directly in the temp buf.
453 | // need to see what percent of CPU this takes up first though...
454 | // Could test a reusable StringBuilder...
455 |
456 | // is this needed here???
457 | // Only if a fieldtype calls writeStr or something
458 | // with a null val instead of calling writeNull
459 | /***
460 | if (val==null) {
461 | if (name==null) writer.write(" ");
462 | else writer.write(" ");
463 | }
464 | ***/
465 |
466 | int contentLen=val.length();
467 |
468 | startTag(tag, name, contentLen==0);
469 | if (contentLen==0) return;
470 |
471 | if (escape) {
472 | XML.escapeCharData(val,writer);
473 | } else {
474 | writer.write(val,0,contentLen);
475 | }
476 |
477 | writer.write("");
478 | writer.write(tag);
479 | writer.write('>');
480 | }
481 |
482 |
483 | }
--------------------------------------------------------------------------------
/src/main/java/co/diji/utils/QueryStringDecoder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2009 Red Hat, Inc.
3 | *
4 | * Red Hat licenses this file to you under the Apache License, version 2.0
5 | * (the "License"); you may not use this file except in compliance with the
6 | * License. You may obtain a copy of the License at:
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | * License for the specific language governing permissions and limitations
14 | * under the License.
15 | */
16 | package co.diji.utils;
17 |
18 | import java.io.UnsupportedEncodingException;
19 | import java.net.URI;
20 | import java.net.URLDecoder;
21 | import java.nio.charset.Charset;
22 | import java.nio.charset.UnsupportedCharsetException;
23 | import java.util.ArrayList;
24 | import java.util.Collections;
25 | import java.util.LinkedHashMap;
26 | import java.util.List;
27 | import java.util.Map;
28 |
29 | /**
30 | * Splits an HTTP query string into a path string and key-value parameter pairs.
31 | * This decoder is for one time use only. Create a new instance for each URI:
32 | *
33 | * {@link QueryStringDecoder} decoder = new {@link QueryStringDecoder}("/hello?recipient=world&x=1;y=2");
34 | * assert decoder.getPath().equals("/hello");
35 | * assert decoder.getParameters().get("recipient").equals("world");
36 | * assert decoder.getParameters().get("x").equals("1");
37 | * assert decoder.getParameters().get("y").equals("2");
38 | *
39 | *
40 | * @author The Netty Project
41 | * @author Andy Taylor (andy.taylor@jboss.org)
42 | * @author Trustin Lee
43 | * @author Benoit Sigoure
44 | * @version $Rev: 2302 $, $Date: 2010-06-14 20:07:44 +0900 (Mon, 14 Jun 2010) $
45 | *
46 | * @see QueryStringEncoder
47 | *
48 | * @apiviz.stereotype utility
49 | * @apiviz.has org.jboss.netty.handler.codec.http.HttpRequest oneway - - decodes
50 | */
51 | public class QueryStringDecoder {
52 |
53 | private final Charset charset;
54 | private final String uri;
55 | private String path;
56 | private Map> params;
57 |
58 | /**
59 | * Creates a new decoder that decodes the specified URI. The decoder will
60 | * assume that the query string is encoded in UTF-8.
61 | */
62 | public QueryStringDecoder(String uri) {
63 | this(uri, Charset.forName("UTF-8"));
64 | }
65 |
66 | /**
67 | * Creates a new decoder that decodes the specified URI encoded in the
68 | * specified charset.
69 | */
70 | public QueryStringDecoder(String uri, Charset charset) {
71 | if (uri == null) {
72 | throw new NullPointerException("uri");
73 | }
74 | if (charset == null) {
75 | throw new NullPointerException("charset");
76 | }
77 |
78 | // http://en.wikipedia.org/wiki/Query_string
79 | this.uri = uri.replace(';', '&');
80 | this.charset = charset;
81 | }
82 |
83 | /**
84 | * @deprecated Use {@link #QueryStringDecoder(String, Charset)} instead.
85 | */
86 | @Deprecated
87 | public QueryStringDecoder(String uri, String charset) {
88 | this(uri, Charset.forName(charset));
89 | }
90 |
91 | /**
92 | * Creates a new decoder that decodes the specified URI. The decoder will
93 | * assume that the query string is encoded in UTF-8.
94 | */
95 | public QueryStringDecoder(URI uri) {
96 | this(uri, Charset.forName("UTF-8"));
97 | }
98 |
99 | /**
100 | * Creates a new decoder that decodes the specified URI encoded in the
101 | * specified charset.
102 | */
103 | public QueryStringDecoder(URI uri, Charset charset){
104 | if (uri == null) {
105 | throw new NullPointerException("uri");
106 | }
107 | if (charset == null) {
108 | throw new NullPointerException("charset");
109 | }
110 |
111 | // http://en.wikipedia.org/wiki/Query_string
112 | this.uri = uri.toASCIIString().replace(';', '&');
113 | this.charset = charset;
114 | }
115 |
116 | /**
117 | * @deprecated Use {@link #QueryStringDecoder(URI, Charset)} instead.
118 | */
119 | @Deprecated
120 | public QueryStringDecoder(URI uri, String charset){
121 | this(uri, Charset.forName(charset));
122 | }
123 |
124 | /**
125 | * Returns the decoded path string of the URI.
126 | */
127 | public String getPath() {
128 | if (path == null) {
129 | int pathEndPos = uri.indexOf('?');
130 | if (pathEndPos < 0) {
131 | path = uri;
132 | }
133 | else {
134 | return path = uri.substring(0, pathEndPos);
135 | }
136 | }
137 | return path;
138 | }
139 |
140 | /**
141 | * Returns the decoded key-value parameter pairs of the URI.
142 | */
143 | public Map> getParameters() {
144 | if (params == null) {
145 | int pathLength = getPath().length();
146 | if (uri.length() == pathLength) {
147 | return Collections.emptyMap();
148 | }
149 | params = decodeParams(uri.substring(pathLength + 1));
150 | }
151 | return params;
152 | }
153 |
154 | private Map> decodeParams(String s) {
155 | Map> params = new LinkedHashMap>();
156 | String name = null;
157 | int pos = 0; // Beginning of the unprocessed region
158 | int i; // End of the unprocessed region
159 | char c = 0; // Current character
160 | for (i = 0; i < s.length(); i++) {
161 | c = s.charAt(i);
162 | if (c == '=' && name == null) {
163 | if (pos != i) {
164 | name = decodeComponent(s.substring(pos, i), charset);
165 | }
166 | pos = i + 1;
167 | } else if (c == '&') {
168 | if (name == null && pos != i) {
169 | // We haven't seen an `=' so far but moved forward.
170 | // Must be a param of the form '&a&' so add it with
171 | // an empty value.
172 | addParam(params, decodeComponent(s.substring(pos, i), charset), "");
173 | } else if (name != null) {
174 | addParam(params, name, decodeComponent(s.substring(pos, i), charset));
175 | name = null;
176 | }
177 | pos = i + 1;
178 | }
179 | }
180 |
181 | if (pos != i) { // Are there characters we haven't dealt with?
182 | if (name == null) { // Yes and we haven't seen any `='.
183 | addParam(params, decodeComponent(s.substring(pos, i), charset), "");
184 | } else { // Yes and this must be the last value.
185 | addParam(params, name, decodeComponent(s.substring(pos, i), charset));
186 | }
187 | } else if (name != null) { // Have we seen a name without value?
188 | addParam(params, name, "");
189 | }
190 |
191 | return params;
192 | }
193 |
194 | private static String decodeComponent(String s, Charset charset) {
195 | if (s == null) {
196 | return "";
197 | }
198 |
199 | try {
200 | return URLDecoder.decode(s, charset.name());
201 | } catch (UnsupportedEncodingException e) {
202 | throw new UnsupportedCharsetException(charset.name());
203 | }
204 | }
205 |
206 | private static void addParam(Map> params, String name, String value) {
207 | List values = params.get(name);
208 | if (values == null) {
209 | values = new ArrayList(1); // Often there's only 1 value.
210 | params.put(name, values);
211 | }
212 | values.add(value);
213 | }
214 | }
--------------------------------------------------------------------------------
/src/main/java/org/elasticsearch/plugin/diji/MockSolrPlugin.java:
--------------------------------------------------------------------------------
1 | package org.elasticsearch.plugin.diji;
2 |
3 | import org.elasticsearch.common.inject.Module;
4 | import org.elasticsearch.plugins.AbstractPlugin;
5 | import org.elasticsearch.rest.RestModule;
6 |
7 | import co.diji.rest.SolrSearchHandlerRestAction;
8 | import co.diji.rest.SolrUpdateHandlerRestAction;
9 |
10 | public class MockSolrPlugin extends AbstractPlugin {
11 | /*
12 | * (non-Javadoc)
13 | *
14 | * @see org.elasticsearch.plugins.Plugin#name()
15 | */
16 | public String name() {
17 | return "MockSolrPlugin";
18 | }
19 |
20 | /*
21 | * (non-Javadoc)
22 | *
23 | * @see org.elasticsearch.plugins.Plugin#description()
24 | */
25 | public String description() {
26 | return "Mocks an instance of Solr";
27 | }
28 |
29 | /*
30 | * (non-Javadoc)
31 | *
32 | * @see
33 | * org.elasticsearch.plugins.AbstractPlugin#processModule(org.elasticsearch.common.inject.Module)
34 | */
35 | @Override
36 | public void processModule(Module module) {
37 | if (module instanceof RestModule) {
38 | ((RestModule) module).addRestAction(SolrUpdateHandlerRestAction.class);
39 | ((RestModule) module).addRestAction(SolrSearchHandlerRestAction.class);
40 | }
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/resources/es-plugin.properties:
--------------------------------------------------------------------------------
1 | plugin=org.elasticsearch.plugin.diji.MockSolrPlugin
2 |
--------------------------------------------------------------------------------