├── .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("'); 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("'); 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 | --------------------------------------------------------------------------------