diff --git a/changelog/unreleased/solr-16640-admin-ui-sql-error-handling.yml b/changelog/unreleased/solr-16640-admin-ui-sql-error-handling.yml new file mode 100644 index 000000000000..9a628d567e51 --- /dev/null +++ b/changelog/unreleased/solr-16640-admin-ui-sql-error-handling.yml @@ -0,0 +1,8 @@ +# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc +title: Admin UI's SQL screen no longer crashes with an uncaught JS error (and silently shows a blank result grid) when the response isn't a SQL result-set - e.g. when the sql module/handler isn't installed. It now shows the server's error message instead. +type: fixed +authors: + - name: Eric Pugh +links: + - name: SOLR-16640 + url: https://issues.apache.org/jira/browse/SOLR-16640 diff --git a/dev-docs/apis.adoc b/dev-docs/apis.adoc index b394b16d4522..ecf7644a5ffd 100644 --- a/dev-docs/apis.adoc +++ b/dev-docs/apis.adoc @@ -76,6 +76,25 @@ Writing a new v2 API may appear daunting, but additions in reality are actually A good example for each of these steps can be seen in Solr's v2 "add-replica-property" API, which has a defining interface https://github.com/apache/solr/blob/9426902acb7081a2e9a1fa29699c5286459e1365/solr/api/src/java/org/apache/solr/client/api/endpoint/AddReplicaPropertyApi.java[AddReplicaPropertyApi], an implementing class https://github.com/apache/solr/blob/9426902acb7081a2e9a1fa29699c5286459e1365/solr/core/src/java/org/apache/solr/handler/admin/api/AddReplicaProperty.java[AddReplicaProperty], and the two POJOs https://github.com/apache/solr/blob/main/solr/api/src/java/org/apache/solr/client/api/model/AddReplicaPropertyRequestBody.java[AddReplicaPropertyRequestBody] and https://github.com/apache/solr/blob/main/solr/api/src/java/org/apache/solr/client/api/model/SolrJerseyResponse.java[SolrJerseyResponse]. +==== HTTP QUERY spike + +Use `QUERY` for safe, idempotent queries whose inputs belong in a request body, as defined by https://www.rfc-editor.org/rfc/rfc10008.html[RFC 10008]. +For example, a search with a structured JSON body containing filters, facets, and sorting is a candidate when expressing those inputs as URL parameters would be cumbersome or exceed URL length limits. +The operation must not request changes to documents, schemas, or cluster configuration, and retrying it must not cause additional mutations. +Results can still change between requests as the underlying index changes; idempotence does not require identical results. + +Prefer `GET` for simple reads or searches that fit naturally in the URL. +Use `POST`, `PUT`, or `DELETE` for operations that request state changes, even if their inputs contain a query (such as delete-by-query). +For body-based reads, `QUERY` communicates safe retry semantics that `POST` does not; retain `POST` where client or intermediary compatibility requires it. +Only use `QUERY` on endpoints that explicitly support it, with a `Content-Type` matching the query body, such as `application/json`. + +The `QUERY` annotation in the `api` module uses JAX-RS's `@HttpMethod("QUERY")` extension mechanism. +Jetty can carry the method as a string without an entry in its `HttpMethod` enum. + +The AngularJS Admin UI's SQL screen offers a GET/POST/QUERY selector for testing against the read-only V1 `/sql` handler; POST is the default. +GET sends the SQL statement as a URL parameter; POST and QUERY send it in a form-encoded body. +Other SolrJ transports, distributed forwarding, authorization policies, OpenAPI generation, and QUERY cache semantics still need validation before adopting it for a public API. + === Relationship Between V1 and V2 Implementations Most v2 APIs have a corresponding legacy v1 API (e.g. `/admin/cores?action=RELOAD` backs `POST /api/cores/coreName/reload`). diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/QUERY.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/QUERY.java new file mode 100644 index 000000000000..7a1bb0b65741 --- /dev/null +++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/QUERY.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.solr.client.api.endpoint; + +import jakarta.ws.rs.HttpMethod; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Marks a JAX-RS resource method as accepting HTTP QUERY requests. */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@HttpMethod("QUERY") +public @interface QUERY {} diff --git a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java index e109022e59ab..6776ce9f7c31 100644 --- a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java +++ b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java @@ -814,7 +814,7 @@ Map startSolr( final var syspropArg = ("techproducts".equals(params.example())) - ? "-Dsolr.modules=clustering,extraction,langid,ltr,scripting -Dsolr.ltr.enabled=true -Dsolr.clustering.enabled=true" + ? "-Dsolr.modules=clustering,extraction,langid,ltr,scripting,sql -Dsolr.ltr.enabled=true -Dsolr.clustering.enabled=true" : ""; String startCmdStr = diff --git a/solr/core/src/test/org/apache/solr/jersey/HttpQueryIntegrationTest.java b/solr/core/src/test/org/apache/solr/jersey/HttpQueryIntegrationTest.java new file mode 100644 index 000000000000..eb2a2d09d4a6 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/jersey/HttpQueryIntegrationTest.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.solr.jersey; + +import static org.apache.solr.SolrTestCaseJ4.TEST_PATH; +import static org.apache.solr.SolrTestCaseJ4.copyMinConf; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import org.apache.solr.SolrTestCase; +import org.apache.solr.api.JerseyResource; +import org.apache.solr.client.api.endpoint.QUERY; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.jetty.HttpJettySolrClient; +import org.apache.solr.client.solrj.request.V2Request; +import org.apache.solr.client.solrj.response.json.JsonMapResponseParser; +import org.apache.solr.common.util.Utils; +import org.apache.solr.handler.RequestHandlerBase; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.response.SolrQueryResponse; +import org.apache.solr.security.AuthorizationContext; +import org.apache.solr.security.PermissionNameProvider; +import org.apache.solr.util.SolrJettyTestRule; +import org.eclipse.jetty.client.StringRequestContent; +import org.eclipse.jetty.http.HttpVersion; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +/** Exercises an extension HTTP method through Jetty, Solr dispatch, and Jersey. */ +public class HttpQueryIntegrationTest extends SolrTestCase { + @ClassRule public static final SolrJettyTestRule solrTestRule = new SolrJettyTestRule(); + + @BeforeClass + public static void setupSolr() throws Exception { + final var solrHome = createTempDir(); + Files.copy(TEST_PATH().resolve("solr.xml"), solrHome.resolve("solr.xml")); + final var coreDir = solrHome.resolve("collection1"); + copyMinConf(coreDir, "name=collection1\n", "solrconfig-minimal.xml"); + final var config = coreDir.resolve("conf/solrconfig.xml"); + Files.writeString( + config, + Files.readString(config) + .replaceAll("]*/>", "") + .replace( + "", + "\n")); + solrTestRule.startSolr(solrHome); + } + + @Test + public void testQueryWithJsonBody() throws Exception { + for (var version : List.of(HttpVersion.HTTP_1_1, HttpVersion.HTTP_2)) { + try (var solrClient = + new HttpJettySolrClient.Builder(solrTestRule.getBaseUrl()) + .useHttp1_1(version == HttpVersion.HTTP_1_1) + .build()) { + final var response = + solrClient + .getHttpClient() + .newRequest(queryUrl()) + .version(version) + .method("QUERY") + .body( + new StringRequestContent( + "application/json", "{\"query\":\"id:42\"}", StandardCharsets.UTF_8)) + .send(); + assertEquals(response.getContentAsString(), 200, response.getStatus()); + assertEquals(version, response.getVersion()); + assertEquals( + "id:42", + ((Map) Utils.fromJSONString(response.getContentAsString())).get("query")); + } + } + } + + @Test + public void testQueryThroughSolrJ() throws Exception { + final var request = + new V2Request.Builder("/cores/collection1/query-spike") + .withMethod(SolrRequest.METHOD.QUERY) + .withPayload("{\"query\":\"id:42\"}") + .build(); + request.setResponseParser(new JsonMapResponseParser()); + assertEquals("id:42", solrTestRule.getJetty().getSolrClient().request(request).get("query")); + } + + @Test + public void testPostDoesNotInvokeQueryEndpoint() throws Exception { + final var response = + solrTestRule + .getJetty() + .getSolrClient() + .getHttpClient() + .newRequest(queryUrl()) + .method("POST") + .body( + new StringRequestContent( + "application/json", "{\"query\":\"id:42\"}", StandardCharsets.UTF_8)) + .send(); + assertEquals(response.getContentAsString(), 405, response.getStatus()); + } + + private String queryUrl() { + return solrTestRule.getJetty().getBaseURLV2() + "/cores/collection1/query-spike"; + } + + /** Registers the test resource with the core's Jersey application. */ + public static class QueryHandler extends RequestHandlerBase { + @Override + public PermissionNameProvider.Name getPermissionName(AuthorizationContext request) { + return PermissionNameProvider.Name.READ_PERM; + } + + @Override + public Boolean registerV2() { + return true; + } + + @Override + public Collection> getJerseyResources() { + return List.of(QueryResource.class); + } + + @Override + public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) {} + + @Override + public String getDescription() { + return "HTTP QUERY spike"; + } + } + + /** Test-only V2 endpoint that echoes the JSON query to verify entity handling. */ + @Path("/cores/{coreName}/query-spike") + public static class QueryResource extends JerseyResource { + @QUERY + @Consumes("application/json") + @Produces("application/json") + @PermissionName(PermissionNameProvider.Name.READ_PERM) + public Map query(Map body) { + return body; + } + } +} diff --git a/solr/solrj-jetty/src/java/org/apache/solr/client/solrj/jetty/HttpJettySolrClient.java b/solr/solrj-jetty/src/java/org/apache/solr/client/solrj/jetty/HttpJettySolrClient.java index cf05fdac07ec..6c7ea58eec4a 100644 --- a/solr/solrj-jetty/src/java/org/apache/solr/client/solrj/jetty/HttpJettySolrClient.java +++ b/solr/solrj-jetty/src/java/org/apache/solr/client/solrj/jetty/HttpJettySolrClient.java @@ -719,11 +719,11 @@ private MakeRequestReturnValue makeRequest( } if (SolrRequest.METHOD.POST == solrRequest.getMethod() - || SolrRequest.METHOD.PUT == solrRequest.getMethod()) { + || SolrRequest.METHOD.PUT == solrRequest.getMethod() + || SolrRequest.METHOD.QUERY == solrRequest.getMethod()) { RequestWriter.ContentWriter contentWriter = requestWriter.getContentWriter(solrRequest); - HttpMethod method = - SolrRequest.METHOD.POST == solrRequest.getMethod() ? HttpMethod.POST : HttpMethod.PUT; + String method = solrRequest.getMethod().toString(); if (contentWriter instanceof RequestWriter.MultipartContentWriter multipartWriter) { // send server list and request list as query string params diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/SolrRequest.java b/solr/solrj/src/java/org/apache/solr/client/solrj/SolrRequest.java index 4e0429e124fd..56f552f192b9 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/SolrRequest.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/SolrRequest.java @@ -63,6 +63,7 @@ public enum METHOD { HEAD, POST, PUT, + QUERY, DELETE; /** @@ -113,6 +114,7 @@ public enum SolrClientContext { METHOD.GET.toString(), METHOD.POST.toString(), METHOD.PUT.toString(), + METHOD.QUERY.toString(), METHOD.DELETE.toString()); private METHOD method = METHOD.GET; diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSqlScreenTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSqlScreenTest.java index c49871834ceb..42a2c6130381 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSqlScreenTest.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSqlScreenTest.java @@ -16,11 +16,13 @@ */ package org.apache.solr.webapp; +import java.util.Locale; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.common.SolrInputDocument; import org.junit.BeforeClass; import org.junit.Test; import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.WebElement; /** @@ -45,16 +47,138 @@ public static void setupCollection() throws Exception { @Test public void testSqlQueryViaUi() { + assertSqlQueryViaUi("POST", "SELECT id FROM " + COLLECTION + " LIMIT 10"); + } + + @Test + public void testSqlQueryViaQuery() { + assertSqlQueryViaUi("QUERY", "SELECT id FROM " + COLLECTION + " LIMIT 10"); + } + + @Test + public void testSqlQueryViaGet() { + assertSqlQueryViaUi("GET", "SELECT id FROM " + COLLECTION + " LIMIT 10"); + } + + @Test + public void testLargeSqlStatementViaPost() { + assertSqlQueryViaUi("POST", largeStatement()); + } + + @Test + public void testLargeSqlStatementViaQuery() { + assertSqlQueryViaUi("QUERY", largeStatement()); + } + + private String largeStatement() { + return "SELECT /* " + "a".repeat(20000) + " */ id FROM " + COLLECTION + " LIMIT 10"; + } + + private void assertSqlQueryViaUi(String method, String statement) { openPage(COLLECTION + "/sqlquery", By.id("sqlquery")); + WebElement selector = waitFor(By.id("httpMethod")); + assertEquals("POST", selector.getDomProperty("value")); + selector.findElement(By.cssSelector("option[value='" + method + "']")).click(); + ((JavascriptExecutor) driver) + .executeScript( + "window.sqlRequest = null;" + + "var originalOpen = XMLHttpRequest.prototype.open;" + + "var originalSend = XMLHttpRequest.prototype.send;" + + "XMLHttpRequest.prototype.open = function(method, url) {" + + " if (/\\/sql(?:\\?|$)/.test(url)) {" + + " this.sqlRequest = window.sqlRequest = {method: method, url: url};" + + " }" + + " return originalOpen.apply(this, arguments);" + + "};" + + "XMLHttpRequest.prototype.send = function(body) {" + + " if (this.sqlRequest) this.sqlRequest.body = body;" + + " return originalSend.apply(this, arguments);" + + "};"); WebElement stmt = waitFor(By.id("sqlexp")); - stmt.clear(); - stmt.sendKeys("SELECT id FROM " + COLLECTION + " LIMIT 10"); + ((JavascriptExecutor) driver) + .executeScript( + "arguments[0].value = arguments[1];" + + "arguments[0].dispatchEvent(new Event('input', {bubbles: true}));", + stmt, + statement); click(By.xpath("//div[@id='sqlquery']//button[@type='submit']")); // the result grid lists all documents for (int i = 1; i <= 3; i++) { waitForPageContains("sql-doc-" + i); } + assertEquals( + method, ((JavascriptExecutor) driver).executeScript("return window.sqlRequest.method;")); + if ("GET".equals(method)) { + assertNull(((JavascriptExecutor) driver).executeScript("return window.sqlRequest.body;")); + assertEquals( + statement, + ((JavascriptExecutor) driver) + .executeScript( + "return new URL(window.sqlRequest.url, window.location.href).searchParams.get('stmt');")); + } else { + assertEquals( + "stmt=" + statement, + ((JavascriptExecutor) driver) + .executeScript("return decodeURIComponent(window.sqlRequest.body);")); + assertEquals( + false, + ((JavascriptExecutor) driver) + .executeScript("return window.sqlRequest.url.includes('stmt=');")); + } assertNoSevereConsoleErrors(); } + + @Test + public void testFailedRequestShowsErrorInsteadOfCrashing() { + // a nonexistent collection makes the request fail with a response that has no "result-set" + // key - the same shape the server returns when the sql module/handler isn't installed + // (SOLR-16640). Before the fix, parsing this crashed with an uncaught TypeError and the + // screen just stayed blank with no explanation. + openPage("nonexistentcoll/sqlquery", By.id("sqlquery")); + WebElement stmt = waitFor(By.id("sqlexp")); + stmt.clear(); + stmt.sendKeys("SELECT id FROM " + COLLECTION + " LIMIT 10"); + click(By.xpath("//div[@id='sqlquery']//button[@type='submit']")); + waitForTextContains(By.id("sql-response"), "no handler, collection, or core"); + // the request is expected to fail (404) - that's the scenario under test + assertNoSevereConsoleErrors("404 (Not Found)"); + } + + @Test + public void testSqlModuleNotEnabledShowsFriendlyMessage() { + // The real response when the sql module isn't on the classpath (reproduced against an + // actual build without the module): a 500 with this exact error envelope shape, since /sql + // is always nominally registered (lazily) for every core regardless of whether the module + // jar is present - only the first real request reveals the class is missing. Invoking + // showResult() directly with this captured shape, rather than needing a real sql-less + // build, since the webapp test classpath always has the module. + openPage(COLLECTION + "/sqlquery", By.id("sqlquery")); + waitFor(By.id("sqlexp")); + String classNotFoundResponse = + "{\"error\":{\"metadata\":{\"error-class\":\"org.apache.solr.common.SolrException\"," + + "\"root-error-class\":\"java.lang.ClassNotFoundException\"}," + + "\"errorClass\":\"org.apache.solr.common.SolrException\"," + + "\"msg\":\" Error loading class 'solr.SQLHandler'\",\"code\":500}}"; + String sqlError = + (String) + ((JavascriptExecutor) driver) + .executeScript( + "var scope = angular.element(document.getElementById('sqlquery')).scope();" + + "scope.showResult(arguments[0]);" + + "scope.$apply();" + + "return scope.sqlError;", + classNotFoundResponse); + assertTrue( + "should show a friendly message, got: " + sqlError, + sqlError != null && sqlError.toLowerCase(Locale.ROOT).contains("sql module")); + assertTrue( + "should say how to fix it: " + sqlError, + sqlError.toLowerCase(Locale.ROOT).contains("enable")); + WebElement documentation = waitFor(By.cssSelector("#sql-response span a")); + assertEquals( + "https://solr.apache.org/guide/solr/latest/query-guide/sql-query.html", + documentation.getDomAttribute("href")); + assertEquals("_out", documentation.getDomAttribute("target")); + } } diff --git a/solr/webapp/web/js/angular/controllers/sqlquery.js b/solr/webapp/web/js/angular/controllers/sqlquery.js index 7aabc887d5e4..682876995f10 100644 --- a/solr/webapp/web/js/angular/controllers/sqlquery.js +++ b/solr/webapp/web/js/angular/controllers/sqlquery.js @@ -19,6 +19,7 @@ solrAdminApp.controller('SQLQueryController', $scope.resetMenu("sqlquery", Constants.IS_COLLECTION_PAGE); $scope.qt = "sql"; + $scope.httpMethod = "POST"; $scope.doExplanation = false $scope.gridOptions = { enableSorting: false, @@ -31,6 +32,60 @@ solrAdminApp.controller('SQLQueryController', } }; $scope.hostPortContext = $location.absUrl().substr(0,$location.absUrl().indexOf("#")); // For display only + + // Handles both the success and error callbacks below: app.js's global interceptor + // (see failed()'s doNotIntercept branch) routes most failures for this request through + // the "success" callback too, so this must defend against a response that isn't a SQL + // result-set either way (e.g. the sql module/handler isn't installed - SOLR-16640). + $scope.showResult = function(raw) { + $scope.lang = "json"; + $scope.sqlError = null; + $scope.sqlModuleMissing = false; + $scope.sqlData = []; + + var jsonData; + try { + jsonData = JSON.parse(raw); + } catch (e) { + $scope.sqlError = raw; + return; + } + + var docs = jsonData && jsonData['result-set'] && jsonData['result-set'].docs; + if (!docs) { + var err = jsonData && jsonData.error; + if (err && err.metadata && err.metadata['root-error-class'] === 'java.lang.ClassNotFoundException' + && err.msg && err.msg.indexOf('SQLHandler') !== -1) { + $scope.sqlModuleMissing = true; + $scope.sqlError = "The sql module doesn't appear to be enabled on this Solr node."; + } else { + $scope.sqlError = (jsonData && jsonData.message) || (err && err.msg) || raw; + } + return; + } + + //get all docs + for (var i = 0; i < docs.length; i++) { + var doc = docs[i] + //get all the properties + if(doc.hasOwnProperty("EOF")){ + if(doc.hasOwnProperty("EXCEPTION")){ + $scope.sqlError = doc.EXCEPTION + } + } else { + $scope.gridOptions.data.push(doc); + } + } + //Build the columnFields from data + var fields = $scope.gridOptions.data[1]; + for (var property in fields) { + if (fields.hasOwnProperty(property)) { + $scope.gridOptions.columnDefs.push({"name":property, "type":{}}) + } + } + $scope.gridApi.core.notifyDataChange + }; + $scope.doQuery = function() { var params = {}; @@ -47,42 +102,23 @@ solrAdminApp.controller('SQLQueryController', $scope.lang = "json"; $scope.response = null; - $scope.url = ""; $scope.gridOptions.data =[] $scope.gridOptions.columnDefs = [] - var url = Query.url(params); - Query.query(params, function(data) { + $scope.url = Query.url(params); - var jsonData = JSON.parse(data.toJSON().data); - $scope.lang = "json"; - $scope.url = url; - $scope.sqlError = null; - $scope.sqlData = []; - if(jsonData != undefined){ - var docs = jsonData['result-set'].docs - //get all docs - for (var i = 0; i < docs.length; i++) { - var doc = docs[i] - //get all the properties - if(doc.hasOwnProperty("EOF")){ - if(doc.hasOwnProperty("EXCEPTION")){ - $scope.sqlError = doc.EXCEPTION - } - } else { - $scope.gridOptions.data.push(doc); - } - } - } - //Build the columnFields from data - var fields = $scope.gridOptions.data[1]; - for (var property in fields) { - if (fields.hasOwnProperty(property)) { - $scope.gridOptions.columnDefs.push({"name":property, "type":{}}) - } - } - $scope.gridApi.core.notifyDataChange - }); + var onSuccess = function(data) { + $scope.showResult(data.toJSON().data); + }; + var onError = function(rejection) { + $scope.showResult((rejection.data && rejection.data.data) || ("HTTP " + rejection.status + " " + rejection.statusText)); + }; + if ($scope.httpMethod === "GET") { + Query.query(params, onSuccess, onError); + } else { + var sendRequest = $scope.httpMethod === "QUERY" ? Query.queryQuery : Query.queryPost; + sendRequest({core: params.core, handler: params.handler}, params, onSuccess, onError); + } }; } ); diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index 462d684121e0..bae76d8612e0 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -355,25 +355,48 @@ solrAdminServices.factory('Metrics', }]) .factory('Query', ['$resource', function($resource) { - var resource = $resource(':core/:handler', {core: '@core', handler: '@handler', '_':Date.now()}, { - "query": { - method: "GET", - transformResponse: function (data) { - return {data: data} - }, - headers: {doNotIntercept: "true"} - } - }); - resource.url = function(params) { + var toQueryString = function(params) { var qs = []; - for (key in params) { + for (var key in params) { if (key != "core" && key != "handler") { for (var i in params[key]) { qs.push(key + "=" + encodeURIComponent(params[key][i])); } } } - return "" + params.core + "/" + params.handler + "?" + qs.sort().join("&"); + return qs.sort().join("&"); + } + var wrapRawResponse = function(data) { + return {data: data} + } + var resource = $resource(':core/:handler', {core: '@core', handler: '@handler', '_':Date.now()}, { + "query": { + method: "GET", + transformResponse: wrapRawResponse, + headers: {doNotIntercept: "true"} + }, + "queryPost": { + method: "POST", + transformRequest: toQueryString, + transformResponse: wrapRawResponse, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + doNotIntercept: "true" + } + }, + "queryQuery": { + method: "QUERY", + hasBody: true, + transformRequest: toQueryString, + transformResponse: wrapRawResponse, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + doNotIntercept: "true" + } + } + }); + resource.url = function(params) { + return "" + params.core + "/" + params.handler + "?" + toQueryString(params); } return resource; }]) diff --git a/solr/webapp/web/partials/sqlquery.html b/solr/webapp/web/partials/sqlquery.html index c0254d13ac34..4acce9674636 100644 --- a/solr/webapp/web/partials/sqlquery.html +++ b/solr/webapp/web/partials/sqlquery.html @@ -22,13 +22,22 @@ SQL Query Statement + + syntax help
{{hostPortContext}}{{url}} -
{{sqlError}}
+
+ {{sqlError}} + See SQL documentation for how to enable it. +