Sparql client implementation
authorymh <ymh.work@gmail.com>
Thu, 03 Nov 2016 01:52:26 +0100
changeset 387 7fba86fa8604
parent 386 c731ab9b934d
child 388 202b6f209b9a
Sparql client implementation
dev/provisioning/modules/sysconfig/templates/handle/handle_config.dct.erb
server/src/app/Http/Controllers/Sparql/SparqlClientController.php
server/src/app/Libraries/Handle/HandleClient.php
server/src/app/Libraries/Sparql/SparqlClient.php
server/src/app/Libraries/Sparql/SparqlQueryAnalyser.php
server/src/app/Libraries/Sparql/TimeoutSparqlClient.php
server/src/config/corpusparole.php
server/src/public/css/app.css
server/src/resources/assets/js/sparqlclient.js
server/src/resources/assets/sass/_app-core.scss
server/src/resources/views/errors/400.blade.php
server/src/resources/views/errors/408.blade.php
server/src/resources/views/errors/500.blade.php
server/src/resources/views/errors/base-errors.blade.php
server/src/resources/views/sparql/sparqlClientForm.blade.php
server/src/resources/views/sparql/sparqlClientResultBase.blade.php
server/src/resources/views/sparql/sparqlClientResultList.blade.php
server/src/tests/Controllers/DateStatsControllerTest.php
server/src/tests/Controllers/GeoStatsControllerTest.php
server/src/tests/Controllers/ThemeControllerTest.php
server/src/tests/Libraries/Filters/CorpusFilterManagerTest.php
server/src/tests/Libraries/Sparql/SparqlQueryAnalyserTest.php
server/src/tests/Libraries/Sparql/files/SparqlQueryAnalyserTest/prefixes_limit.sparql
--- a/dev/provisioning/modules/sysconfig/templates/handle/handle_config.dct.erb	Mon Oct 31 14:24:23 2016 +0100
+++ b/dev/provisioning/modules/sysconfig/templates/handle/handle_config.dct.erb	Thu Nov 03 01:52:26 2016 +0100
@@ -38,6 +38,9 @@
       "300:0.NA/<%= @hdl_prefix %>"
       "300:0.NA/<%= @hdl_test_prefix %>"
       "300:0.NA/<%= @hdl_test_prefix %>_DSA"
+      "300:<%= @hdl_prefix %>/<%= @hdl_prefix_admin %>"
+      "300:<%= @hdl_test_prefix %>/<%= @hdl_prefix_admin %>"
+      "300:<%= @hdl_test_prefix %>/<%= @hdl_prefix_admin %>_DSA"
    )
 
     "replication_admins" = (
--- a/server/src/app/Http/Controllers/Sparql/SparqlClientController.php	Mon Oct 31 14:24:23 2016 +0100
+++ b/server/src/app/Http/Controllers/Sparql/SparqlClientController.php	Thu Nov 03 01:52:26 2016 +0100
@@ -5,6 +5,9 @@
 use Log;
 
 use Illuminate\Http\Request;
+use Illuminate\Pagination\LengthAwarePaginator;
+use Illuminate\Pagination\Paginator;
+
 use GuzzleHttp\Client;
 use EasyRdf\Sparql\Result as SparqlResult;
 use EasyRdf\Graph;
@@ -52,17 +55,25 @@
         $this->httpClient = $httpClient;
     }
 
+    public function getSparqlClient($timeout = null) {
+        if(is_null($timeout)) {
+            $timeout = config('corpusparole.sparql_client_timeout');
+        }
+        $queryUrl = config('corpusparole.sesame_query_url');
+        if($timeout > 0) {
+            $queryUrl = $queryUrl .
+                ((strlen(parse_url($queryUrl, PHP_URL_QUERY)) > 0)?"&":"?").
+                "timeout=$timeout";
+        }
+        return new \EasyRdf\Sparql\Client($queryUrl, config('corpusparole.sesame_update_url'));
+    }
+
     // display form
     public function index() {
         return view('sparql/sparqlClientForm');
     }
 
-    private function querySelect(Request $request, $query, $analyser) {
-        $countResult = $this->sparqlClient->query($analyser->getCountQuery());
-        $countField = $countResult->getFields()[0];
-        $count = $countResult->current()->$countField;
-        $docs = $this->sparqlClient->query($query);
-        $fields = $docs->getFields();
+    private function readDocs($docs, $fields) {
         $results = [];
         foreach($docs as $row) {
             $results[] = array_reduce($fields, function($res, $field) use ($row) {
@@ -74,10 +85,78 @@
                 return $res;
             }, []);
         }
+        return $results;
+    }
+
+    private function abort($code, $message, $exception) {
+        throw new \Symfony\Component\HttpKernel\Exception\HttpException($code, $message, $exception, []);
+    }
+
+    private function processQueryException($exception) {
+        $message = $exception->getMessage();
+        if($exception instanceof \EasyRdf\Http\Exception) {
+            if(preg_match("/SPARQL\squery/", $message)) {
+                $this->abort(400, "La requête SPARQL n'est pas reconnue", $exception);
+            } else {
+                $this->abort(500, "Problème HTTP lors de la requête SPARQL", $exception);
+            }
+        } elseif($exception instanceof \EasyRdf\Exception) {
+            if(preg_match("/timed\sout/i", $message)) {
+                $this->abort(408, "Time-out causé par la requête SPARQL", $exception);
+            } else {
+                $this->abort(500, "Problème dans la requête SPARQL", $exception);
+            }
+        } else {
+            $this->abort(500, "Erreur serveur lors de la requête", $exception);
+        }
+    }
+
+    private function querySelect(Request $request, $query, $analyser) {
+
+        $limit = intval($request->input('limit', config('corpusparole.sparql_client_default_limit')));
+
+        if($limit === 0 || !is_null($analyser->getLimit()) || !is_null($analyser->getOffset()) ) {
+            try {
+                $docs = $this->getSparqlClient()->query($query);
+            } catch(\Exception $exception) {
+                $this->processQueryException($exception);
+            }
+
+            $fields = $docs->getFields();
+            $results = $this->readDocs($docs, $fields);
+            $count = count($results);
+
+        } else {
+
+            $page = Paginator::resolveCurrentPage(config('corpusparole.pagination_page_param'));
+            assert(is_null($page) || is_numeric($page));
+
+            $offset = max(0,($page - 1) * $limit);
+
+            try {
+                $countResult = $this->getSparqlClient()->query($analyser->getCountQuery());
+                $docs = $this->getSparqlClient()->query($query . " LIMIT $limit OFFSET $offset");
+            } catch(\Exception $exception) {
+                $this->processQueryException($exception);
+            }
+
+
+            $countField = $countResult->getFields()[0];
+            $count = $countResult->current()->$countField->getValue();
+
+            $fields = $docs->getFields();
+
+            $results = new LengthAwarePaginator($this->readDocs($docs, $fields), $count, $limit, $page, [
+                'path' => Paginator::resolveCurrentPath(),
+                'pageName' => config('corpusparole.pagination_page_param'),
+            ]);
+        }
+
         $namespaces = array_reduce(array_keys(RdfHelper::getPrefixes()), function($res, $p) {
             $res[$p] = RdfNamespace::namespaces()[$p];
             return $res;
         }, []);
+
         $data = [
             'query' => $query,
             'count' => $count,
@@ -92,8 +171,13 @@
     }
 
     private function queryGraph(Request $request, $query, $analyser) {
+        try {
+            $docs = $this->getSparqlClient()->query($query);
+        } catch(\Exception $exception) {
+            $this->processQueryException($exception);
+        }
 
-        $docs = $this->sparqlClient->query($query);
+
         $fields = ["subject", "predicate", "object"];
         $results = [];
         foreach ($docs->resources() as $resource ) {
@@ -129,8 +213,14 @@
     }
 
     private function queryAsk(Request $request, $query, $analyser) {
+        try {
+            $result = $this->getSparqlClient()->query($query);
+        } catch(\Exception $exception) {
+            $this->processQueryException($exception);
+        }
+
         $data = [
-            'results' => $this->sparqlClient->query($query),
+            'results' => $result,
             'namespaces' => $analyser->getPrefixes()
         ];
 
@@ -159,8 +249,7 @@
         } elseif($queryType === SparqlQueryAnalyser::ASK_QUERY) {
             list($view, $data) = $this->queryAsk($request, $query, $analyser);
         } else {
-            //return 500
-            abort(500, "Serialization format unknown");
+            abort(400, "La requête n'est pas reconnue");
         }
 
         return view($view, $data);
@@ -182,7 +271,30 @@
             $headers['Accept'] = $format;
         }
 
-        $sesameResp = $this->httpClient->get(config('corpusparole.sesame_query_url'), ['query' => $request->all(), 'headers' => $headers]);
+        $queryParams = $request->all();
+        $queryParams['timeout'] = config('corpusparole.sparql_client_timeout');
+        $queryUrl = config('corpusparole.sesame_query_url');
+
+        try {
+            $sesameResp = $this->httpClient->post($queryUrl, ['form_params' => $queryParams, 'headers' => $headers]);
+        } catch(\GuzzleHttp\Exception\ServerException $exception) {
+            if($exception->getCode() == 503) {
+                $this->abort(408, "Time-out causé par la requête SPARQL", $exception);
+            } else {
+                $this->abort(500, "Problème lors de la requête SPARQL", $exception);
+            }
+
+        } catch(\GuzzleHttp\Exception\RequestException $exception) {
+            $message = $exception->getMessage();
+            if(preg_match("/MALFORMED\sQUERY/i", $message)) {
+                $abortMessage = "Requête SPARQL mal-formée";
+            } else {
+                $abortMessage = "Problème lors de la requête SPARQL";
+            }
+            $this->abort($exception->getCode(), $abortMessage, $exception);
+        } catch(\Exception $exception) {
+            $this->abort(500, "Erreur serveur lors de la requête", $exception);
+        }
 
         $resp = response((string)$sesameResp->getBody(), $sesameResp->getStatusCode());
         foreach ($sesameResp->getHeaders() as $name => $values) {
--- a/server/src/app/Libraries/Handle/HandleClient.php	Mon Oct 31 14:24:23 2016 +0100
+++ b/server/src/app/Libraries/Handle/HandleClient.php	Thu Nov 03 01:52:26 2016 +0100
@@ -180,9 +180,10 @@
             ];
             # Send the request again with a valid correctly signed Authorization header
             $sessionResp = $this->httpClient->put($url.'this', ['headers' => $pkheaders, 'verify' => false]);
-            Log::debug('Create session with auth: '.$sessionResp->getStatusCode().' : '.$sessionResp->getReasonPhrase());
+
 
             $jsonResp = json_decode($sessionResp->getBody(), true);
+            Log::debug('Create session with auth: '.$sessionResp->getStatusCode().' : '.$sessionResp->getReasonPhrase(). " with body : \n".$sessionResp->getBody());
 
             $this->session = $jsonResp['authenticated']?$jsonResp['sessionId']:"";
             $headers['Authorization'] = "Handle version=\"0\", sessionId=\"$this->session\"";
--- a/server/src/app/Libraries/Sparql/SparqlClient.php	Mon Oct 31 14:24:23 2016 +0100
+++ b/server/src/app/Libraries/Sparql/SparqlClient.php	Thu Nov 03 01:52:26 2016 +0100
@@ -167,8 +167,8 @@
      * @param string $query The query string to be executed
      * @return object EasyRdf\Sparql\Result|EasyRdf\Graph Result of the query.
      */
-    public function query($query) {
-        return $this->sparqlClient->query($query);
+    public function query($query, $timeout=0) {
+        return $this->sparqlClient->query($query, $timeout);
     }
 
 }
--- a/server/src/app/Libraries/Sparql/SparqlQueryAnalyser.php	Mon Oct 31 14:24:23 2016 +0100
+++ b/server/src/app/Libraries/Sparql/SparqlQueryAnalyser.php	Thu Nov 03 01:52:26 2016 +0100
@@ -47,35 +47,44 @@
         return $this->queryType;
     }
 
-    private function extractPrefix() {
-        $prefixes = [];
-        $rawPrefixes = [];
-        $res = preg_replace_callback("%".self::SPARQL_PREFIX_BASE_REGEXP."%iu", function($m) use (&$prefixes, &$rawPrefixes) {
-            $rawPrefixes[] = trim($m[0]);
-            $prefixes[$m[3]?$m[3]:""] = $m[4];
+    private function extractPrefixLimit() {
+        $this->prefixes = [];
+        $this->rawPrefixes = [];
+        $res = preg_replace_callback("%".self::SPARQL_PREFIX_BASE_REGEXP."%iu", function($m) {
+            $this->rawPrefixes[] = trim($m[0]);
+            $this->prefixes[$m[3]?$m[3]:""] = $m[4];
             return "";
         }, $this->query);
+        $res = preg_replace_callback("%".self::SPARQL_LIMIT_OFFSET_QUERY_REGEXP."%iu", function($m) {
+            for($i=0;$i<(count($m)-1)/2;$i++) {
+                if(Utils::startsWith(strtolower($m[2*$i+1]), "limit")) {
+                    $this->limit = intval($m[$i*2+2]);
+                } elseif (Utils::startsWith(strtolower($m[2*$i+1]), "offset")) {
+                    $this->offset = intval($m[$i*2+2]);
+                }
+            }
+        }, $res);
 
-        return [$rawPrefixes, $prefixes, trim($res)];
+        $this->rawQuery = trim($res);
     }
 
     public function getRawPrefixes() {
         if($this->rawPrefixes === false) {
-            list($this->rawPrefixes, $this->prefixes, $this->rawQuery) = $this->extractPrefix();
+            $this->extractPrefixLimit();
         }
         return $this->rawPrefixes;
     }
 
     public function getPrefixes() {
         if($this->prefixes === false) {
-            list($this->rawPrefixes, $this->prefixes, $this->rawQuery) = $this->extractPrefix();
+            $this->extractPrefixLimit();
         }
         return $this->prefixes;
     }
 
     public function getRawQuery() {
         if($this->rawQuery === false) {
-            list($this->rawPrefixes, $this->prefixes, $this->rawQuery) = $this->extractPrefix();
+            $this->extractPrefixLimit();
         }
         return $this->rawQuery;
     }
@@ -92,15 +101,7 @@
     }
 
     private function setLimitOffset() {
-        if(preg_match("%".self::SPARQL_LIMIT_OFFSET_QUERY_REGEXP."%iu", $this->query, $m) === 1) {
-            for($i=0;$i<(count($m)-1)/2;$i++) {
-                if(Utils::startsWith(strtolower($m[2*$i+1]), "limit")) {
-                    $this->limit = intval($m[$i*2+2]);
-                } elseif (Utils::startsWith(strtolower($m[2*$i+1]), "offset")) {
-                    $this->offset = intval($m[$i*2+2]);
-                }
-            }
-        }
+        $this->extractPrefixLimit();
         if($this->limit === false) {
             $this->limit = null;
         }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/src/app/Libraries/Sparql/TimeoutSparqlClient.php	Thu Nov 03 01:52:26 2016 +0100
@@ -0,0 +1,100 @@
+<?php
+namespace CorpusParole\Libraries\Sparql;
+
+use EasyRdf\Sparql\Client;
+use EasyRdf\Exception;
+use EasyRdf\Format;
+use EasyRdf\Http;
+
+class TimeoutSparqlClient extends Client {
+
+    public function query($query, $timeout=0)
+    {
+        return $this->request('query', $query, $timeout);
+    }
+
+    protected function request($type, $query, $timeout=0)
+    {
+        $processed_query = $this->preprocessQuery($query);
+        $response = $this->executeQuery($processed_query, $type, $timeout);
+        if (!$response->isSuccessful()) {
+            throw new Http\Exception("HTTP request for SPARQL query failed", 0, null, $response->getBody());
+        }
+        if ($response->getStatus() == 204) {
+            // No content
+            return $response;
+        }
+        return $this->parseResponseToQuery($response);
+    }
+
+    /**
+     * Build http-client object, execute request and return a response
+     *
+     * @param string $processed_query
+     * @param string $type            Should be either "query" or "update"
+     *
+     * @return Http\Response|\Zend\Http\Response
+     * @throws Exception
+     */
+    protected function executeQuery($processed_query, $type, $timeout=0)
+    {
+        $client = Http::getDefaultHttpClient();
+        $client->resetParameters();
+        // Tell the server which response formats we can parse
+        $sparql_results_types = array(
+            'application/sparql-results+json' => 1.0,
+            'application/sparql-results+xml' => 0.8
+        );
+        if ($type == 'update') {
+            // accept anything, as "response body of a […] update request is implementation defined"
+            // @see http://www.w3.org/TR/sparql11-protocol/#update-success
+            $accept = Format::getHttpAcceptHeader($sparql_results_types);
+            $client->setHeaders('Accept', $accept);
+            $client->setMethod('POST');
+            $client->setUri($this->updateUri);
+            $client->setRawData($processed_query);
+            $client->setHeaders('Content-Type', 'application/sparql-update');
+        } elseif ($type == 'query') {
+            $re = '(?:(?:\s*BASE\s*<.*?>\s*)|(?:\s*PREFIX\s+.+:\s*<.*?>\s*))*'.
+                '(CONSTRUCT|SELECT|ASK|DESCRIBE)[\W]';
+            $result = null;
+            $matched = mb_eregi($re, $processed_query, $result);
+            if (false === $matched or count($result) !== 2) {
+                // non-standard query. is this something non-standard?
+                $query_verb = null;
+            } else {
+                $query_verb = strtoupper($result[1]);
+            }
+            if ($query_verb === 'SELECT' or $query_verb === 'ASK') {
+                // only "results"
+                $accept = Format::formatAcceptHeader($sparql_results_types);
+            } elseif ($query_verb === 'CONSTRUCT' or $query_verb === 'DESCRIBE') {
+                // only "graph"
+                $accept = Format::getHttpAcceptHeader();
+            } else {
+                // both
+                $accept = Format::getHttpAcceptHeader($sparql_results_types);
+            }
+            $client->setHeaders('Accept', $accept);
+            $encodedQuery = 'query=' . urlencode($processed_query).(($timeout>0)?"&timeout=$timeout":"");
+            // Use GET if the query is less than 2kB
+            // 2046 = 2kB minus 1 for '?' and 1 for NULL-terminated string on server
+            if (strlen($encodedQuery) + strlen($this->queryUri) <= 2046) {
+                $delimiter = $this->queryUri_has_params ? '&' : '?';
+                $client->setMethod('GET');
+                $client->setUri($this->queryUri . $delimiter . $encodedQuery);
+            } else {
+                // Fall back to POST instead (which is un-cacheable)
+                $client->setMethod('POST');
+                $client->setUri($this->queryUri);
+                $client->setRawData($encodedQuery);
+                $client->setHeaders('Content-Type', 'application/x-www-form-urlencoded');
+            }
+        } else {
+            throw new Exception('unexpected request-type: '.$type);
+        }
+        return $client->request();
+    }
+
+
+}
--- a/server/src/config/corpusparole.php	Mon Oct 31 14:24:23 2016 +0100
+++ b/server/src/config/corpusparole.php	Thu Nov 03 01:52:26 2016 +0100
@@ -383,6 +383,9 @@
     'filter_max_languages_nb'=> 200,
     'filter_max_themes_nb'=> 200,
     'filter_max_discourses_nb'=> 200,
-    'filter_max_dates_nb'=> 200
+    'filter_max_dates_nb'=> 200,
+
+    'sparql_client_timeout' => 5,
+    'sparql_client_default_limit' => 100
 
 ];
--- a/server/src/public/css/app.css	Mon Oct 31 14:24:23 2016 +0100
+++ b/server/src/public/css/app.css	Thu Nov 03 01:52:26 2016 +0100
@@ -6061,4 +6061,13 @@
 .corpus-rdf-boolean-false {
   color: #a11; }
 
+.error-btn.collapsed .caret {
+  border-top: 0;
+  border-bottom: 4px dashed;
+  border-bottom: 4px solid \9;
+  content: ""; }
+
+#baseExceptionCollapse {
+  margin-top: 10px; }
+
 /*# sourceMappingURL=app.css.map */
--- a/server/src/resources/assets/js/sparqlclient.js	Mon Oct 31 14:24:23 2016 +0100
+++ b/server/src/resources/assets/js/sparqlclient.js	Thu Nov 03 01:52:26 2016 +0100
@@ -69,11 +69,6 @@
     var yasqe = YASQE.fromTextArea($('#query').get(0), {
         sparql: {
             showQueryButton: false,
-            //endpoint: "{{ route('sparql_proxy') }}",
-            //requestMethod: "GET",
-            //acceptHeaderGraph: "application/rdf+json,/;q=0.9"
-            //acceptHeaderGraph: "text/turtle,/;q=0.9",
-            //acceptHeaderSelect: "application/x-turtle,/;q=0.9",
         }
     });
     yasqe.on("update", function(instance) {
@@ -94,10 +89,12 @@
         $('#limit').val($(e.target).text());
     });
 
-    // var yasr = YASR($('#results').get(0), {
-    //     getUsedPrefixes: yasqe.getPrefixesFromQuery,
-    //     useGoogleCharts: false,
-    //     outputPlugins: ["table", "error", "boolean", "rawResponse", "pivot"],
-    // });
-    // yasqe.options.sparql.callbacks.complete = yasr.setResponse;
+    $("#reset-query").click(function(e) {
+        yasqe.setValue("");
+    });
+
+    $("#format").change(function(e) {
+        $("#limit, #limit-btn").prop('disabled', ($(e.target).val() != "text/html") );
+    });
+
 }
--- a/server/src/resources/assets/sass/_app-core.scss	Mon Oct 31 14:24:23 2016 +0100
+++ b/server/src/resources/assets/sass/_app-core.scss	Thu Nov 03 01:52:26 2016 +0100
@@ -151,3 +151,14 @@
 .corpus-rdf-boolean-false {
     color: #a11;
 }
+
+.error-btn.collapsed .caret {
+    border-top: 0;
+    border-bottom: $caret-width-base dashed;
+    border-bottom: $caret-width-base solid \9; // IE8
+    content: "";
+}
+
+#baseExceptionCollapse {
+    margin-top: 10px;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/src/resources/views/errors/400.blade.php	Thu Nov 03 01:52:26 2016 +0100
@@ -0,0 +1,1 @@
+@extends('errors/base-errors')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/src/resources/views/errors/408.blade.php	Thu Nov 03 01:52:26 2016 +0100
@@ -0,0 +1,1 @@
+@extends('errors/base-errors')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/src/resources/views/errors/500.blade.php	Thu Nov 03 01:52:26 2016 +0100
@@ -0,0 +1,2 @@
+@extends('errors/base-errors')
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/src/resources/views/errors/base-errors.blade.php	Thu Nov 03 01:52:26 2016 +0100
@@ -0,0 +1,17 @@
+@extends('base')
+
+@section('content')
+<div class="row">
+    <div class="col-md-12">
+        <h1 class="text-danger">Code {{ $exception->getStatusCode() }} : Erreur lors de la requête </h1>
+        <p class="lead">{{ $exception->getMessage() }}</p>
+        <button class="btn btn-default collapsed btn-xs error-btn" type="button" data-toggle="collapse" data-target="#baseExceptionCollapse" aria-expanded="false" aria-controls="detailException">
+            <span class="caret"></span>
+        </button>
+        <div class="collapse" id="baseExceptionCollapse">
+            <div class="well">{{ $exception->getPrevious()->getCode() }}: <code>{{ htmlspecialchars($exception->getPrevious()->getMessage(), ENT_SUBSTITUTE) }}</code></div>
+        </div>
+    </div>
+</div>
+
+@endsection
--- a/server/src/resources/views/sparql/sparqlClientForm.blade.php	Mon Oct 31 14:24:23 2016 +0100
+++ b/server/src/resources/views/sparql/sparqlClientForm.blade.php	Thu Nov 03 01:52:26 2016 +0100
@@ -20,22 +20,6 @@
     <script>
     $(function() {
         initSparqlEditor();
-        // var yasqe = YASQE.fromTextArea($('#query').get(0), {
-        //     sparql: {
-        //         showQueryButton: false,
-        //         //endpoint: "{{ route('sparql_proxy') }}",
-        //         //requestMethod: "GET",
-        //         //acceptHeaderGraph: "application/rdf+json,/;q=0.9"
-        //         //acceptHeaderGraph: "text/turtle,/;q=0.9",
-        //         //acceptHeaderSelect: "application/x-turtle,/;q=0.9",
-        //     }
-        // });
-        // // var yasr = YASR($('#results').get(0), {
-        // //     getUsedPrefixes: yasqe.getPrefixesFromQuery,
-        // //     useGoogleCharts: false,
-        // //     outputPlugins: ["table", "error", "boolean", "rawResponse", "pivot"],
-        // // });
-        // // yasqe.options.sparql.callbacks.complete = yasr.setResponse;
     });
     </script>
 @endsection
@@ -59,16 +43,11 @@
                         </select>
                     </div>
                     <div class="form-group col-md-2">
-                        <label for="timeout" class="n control-label">Timeout</label>
-                        <input name="timeout" id="timeout" class="form-control" type="text" value="0" />
-                        <span class=" help-block"> milliseconds <i>(values less than 1000 are ignored)</i></span>
-                    </div>
-                    <div class="form-group col-md-2">
                         <label for="limit" class="n control-label">Limite</label>
                         <div class="input-group">
-                            <input name="limit" id="limit" class="form-control" type="text" value="0" />
+                            <input name="limit" id="limit" class="form-control" type="text" value="{{ config('corpusparole.sparql_client_default_limit') }}" />
                             <div class="input-group-btn">
-                                <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><span class="caret"></span></button>
+                                <button id="limit-btn" type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><span class="caret"></span></button>
                                 <ul id="limits-choices" class="dropdown-menu dropdown-menu-right">
                                     <li><a href="#">0</a></li>
                                     <li><a href="#">100</a></li>
@@ -83,7 +62,6 @@
                 <div class="form-group row">
                     <div class="col-md-12">
                     <input id="submit-query-form" type="submit" class="btn btn-primary" value="Lancer la requête"/>
-                    <input type="reset" class="btn" value="URL de la requête" id="get-query-url"/>
                     <input type="reset" class="btn" value="Nouvelle requête" id="reset-query"/>
                     </div>
                 </div>
--- a/server/src/resources/views/sparql/sparqlClientResultBase.blade.php	Mon Oct 31 14:24:23 2016 +0100
+++ b/server/src/resources/views/sparql/sparqlClientResultBase.blade.php	Thu Nov 03 01:52:26 2016 +0100
@@ -1,5 +1,11 @@
 @extends('base')
 
+@section('nav')
+<ul class="nav navbar-nav">
+    <li><a href="{{ route('sparql_form') }}">Client SPARQL</a></li>
+</ul>
+@endsection
+
 @section('content')
 <div class="row">
     <div class="col-md-12">
--- a/server/src/resources/views/sparql/sparqlClientResultList.blade.php	Mon Oct 31 14:24:23 2016 +0100
+++ b/server/src/resources/views/sparql/sparqlClientResultList.blade.php	Thu Nov 03 01:52:26 2016 +0100
@@ -1,49 +1,72 @@
 @extends('sparql/sparqlClientResultBase')
 
 @section('result-content')
-<div class="result-bindings-heading panel-heading h3">Résultat de la requête ({{$count}})</div>
+<div class="result-bindings-heading panel-heading h3">Résultat de la requête
+@if($results instanceof Illuminate\Pagination\LengthAwarePaginator)
+ ({{ $results->firstItem() }}-{{ $results->lastItem() }} de {{ $results->total() }})
+@else
+ ({{$count}})
+@endif
+</div>
 <div class="result-bindings-body panel-body">
-    <div class="col-md-6">
-    @if(count($namespaces)>0)
-        <table class="result-bindings-namespaces table table-striped">
-        <caption>Namespaces</caption>
-        @foreach ($namespaces as $short => $uri)
-            <tr><td class="result-bindings-namespaces-prefix-cell"><span class="result-bindings-namespaces-prefix">{{$short}}</span>:</td><td>{{$uri}}</td></tr>
-        @endforeach
-        </table>
-    @endif
+    <div class="row">
+        <div class="col-md-6">
+        @if(count($namespaces)>0)
+            <table class="result-bindings-namespaces table table-striped">
+            <caption>Namespaces</caption>
+            @foreach ($namespaces as $short => $uri)
+                <tr><td class="result-bindings-namespaces-prefix-cell"><span class="result-bindings-namespaces-prefix">{{$short}}</span>:</td><td>{{$uri}}</td></tr>
+            @endforeach
+            </table>
+        @endif
+        </div>
+        <div class="col-md-6">
+            <form action="{{ route('sparql_query') }}" method="get" class="form-inline">
+            <input type="hidden" name="query" value="{{ $query }}"/>
+            <div class="form-group">
+            <label for="format" class="n control-label">Download format</label>
+            <select name="format" id="format" class="form-control">
+                @foreach($downloadFormats as $t => $v)
+                <option value="{{ $v }}">{{ $t }}</option>
+                @endforeach
+            </select>
+            </div>
+            <input id="submit-query-form" type="submit" class="btn btn-primary" value="download"/>
+            <form>
+        </div>
     </div>
-    <div class="col-md-6">
-        <form action="{{ route('sparql_query') }}" method="get" class="form-inline">
-        <input type="hidden" name="query" value="{{ $query }}"/>
-        <div class="form-group">
-        <label for="format" class="n control-label">Download format</label>
-        <select name="format" id="format" class="form-control">
-            @foreach($downloadFormats as $t => $v)
-            <option value="{{ $v }}">{{ $t }}</option>
-            @endforeach
-        </select>
+    @if($results instanceof Illuminate\Pagination\LengthAwarePaginator)
+    <div class="row">
+        <div class="col-md-12">
+            {{ $results->links() }}
         </div>
-        <input id="submit-query-form" type="submit" class="btn btn-primary" value="download"/>
-        <form>
     </div>
+    @endif
 </div>
-<table class='sparql-results table table-striped table-hover'>
-<thead>
-<tr>
-    @foreach ($fields as $field)
-    <th>{{ $fieldPrefix }}{{ $field }}</th>
-    @endforeach
-</tr>
-</thead>
-<tbody>
-    @foreach ($results as $row)
+<div class="table-responsive">
+    <table class='sparql-results table table-striped table-hover'>
+    <thead>
     <tr>
         @foreach ($fields as $field)
-        <td>{!! $row[$field] !!}</td>
+        <th>{{ $fieldPrefix }}{{ $field }}</th>
         @endforeach
     </tr>
-    @endforeach
-</tbody>
-</table>
+    </thead>
+    <tbody>
+        @foreach ($results as $row)
+        <tr>
+            @foreach ($fields as $field)
+            <td>{!! $row[$field] !!}</td>
+            @endforeach
+        </tr>
+        @endforeach
+    </tbody>
+    </table>
+</div>
+@if($results instanceof Illuminate\Pagination\LengthAwarePaginator)
+<div class="panel-footer">
+    {{ $results->links() }}
+</div>
+@endif
+
 @endsection
--- a/server/src/tests/Controllers/DateStatsControllerTest.php	Mon Oct 31 14:24:23 2016 +0100
+++ b/server/src/tests/Controllers/DateStatsControllerTest.php	Thu Nov 03 01:52:26 2016 +0100
@@ -2,8 +2,6 @@
 
 use Mockery as m;
 
-use Es;
-
 use EasyRdf\Literal;
 
 class DateStatsControllerTest extends TestCase
--- a/server/src/tests/Controllers/GeoStatsControllerTest.php	Mon Oct 31 14:24:23 2016 +0100
+++ b/server/src/tests/Controllers/GeoStatsControllerTest.php	Thu Nov 03 01:52:26 2016 +0100
@@ -116,13 +116,20 @@
 
     public function testGetIndexArea()
     {
+
         $query = [
-            'index' => env('ELASTICSEARCH_INDEX'),
-            'body' => [
+            "index" => env('ELASTICSEARCH_INDEX'),
+            "body" => [
                 "size" => 0,
                 "query" => [
-                    'term' => [
-                        "geonames_hierarchy" => "code_area"
+                    "constant_score" => [
+                        "filter" => [
+                            "bool" => [
+                                "must" => [ [
+                                    "term" => [ "geonames_hierarchy" => "code_area" ]
+                                ] ]
+                            ]
+                        ]
                     ]
                 ],
                 "aggs" => [
--- a/server/src/tests/Controllers/ThemeControllerTest.php	Mon Oct 31 14:24:23 2016 +0100
+++ b/server/src/tests/Controllers/ThemeControllerTest.php	Thu Nov 03 01:52:26 2016 +0100
@@ -27,6 +27,9 @@
             'index' => env('ELASTICSEARCH_INDEX'),
             'body' => [
                 'size' => 0,
+                'query' =>[
+                    'match_all' => []
+                ],
                 'aggs' => [
                     "subjects" => [
                         "nested" => [ "path" => "subject" ],
@@ -116,6 +119,9 @@
             'index' => env('ELASTICSEARCH_INDEX'),
             'body' => [
                 'size' => 0,
+                'query' =>[
+                    'match_all' => []
+                ],
                 'aggs' => [
                     "subjects" => [
                         "nested" => [ "path" => "subject" ],
@@ -212,6 +218,9 @@
             'index' => env('ELASTICSEARCH_INDEX'),
             'body' => [
                 'size' => 0,
+                'query' =>[
+                    'match_all' => []
+                ],
                 'aggs' => [
                     "subjects" => [
                         "nested" => [ "path" => "subject" ],
@@ -276,6 +285,9 @@
             'index' => env('ELASTICSEARCH_INDEX'),
             'body' => [
                 'size' => 0,
+                'query' =>[
+                    'match_all' => []
+                ],
                 'aggs' => [
                     "subjects" => [
                         "nested" => [ "path" => "subject" ],
@@ -372,6 +384,9 @@
             'index' => env('ELASTICSEARCH_INDEX'),
             'body' => [
                 'size' => 0,
+                'query' =>[
+                    'match_all' => []
+                ],
                 'aggs' => [
                     "subjects" => [
                         "nested" => [ "path" => "subject" ],
@@ -457,6 +472,9 @@
             'index' => env('ELASTICSEARCH_INDEX'),
             'body' => [
                 'size' => 0,
+                'query' =>[
+                    'match_all' => []
+                ],
                 'aggs' => [
                     "subjects" => [
                         "nested" => [ "path" => "subject" ],
@@ -542,6 +560,9 @@
             'index' => env('ELASTICSEARCH_INDEX'),
             'body' => [
                 'size' => 0,
+                'query' =>[
+                    'match_all' => []
+                ],
                 'aggs' => [
                     "subjects" => [
                         "nested" => [ "path" => "subject" ],
--- a/server/src/tests/Libraries/Filters/CorpusFilterManagerTest.php	Mon Oct 31 14:24:23 2016 +0100
+++ b/server/src/tests/Libraries/Filters/CorpusFilterManagerTest.php	Thu Nov 03 01:52:26 2016 +0100
@@ -12,7 +12,8 @@
     public function testPrepareLanguagesNoOp()
     {
         $languagesInput = ['http://lexvo.org/id/iso639-3/fra', 'http://lexvo.org/id/iso639-3/gsw', 'http://lexvo.org/id/iso639-3/bre', 'http://lexvo.org/id/iso639-3/oci'];
-        $languagesOutput = CorpusFilterManager::prepareLanguages($languagesInput);
+        $filterManager = new CorpusFilterManager();
+        $languagesOutput = $filterManager->prepareLanguages($languagesInput);
         $this->assertEquals($languagesInput, $languagesOutput);
     }
 
@@ -24,7 +25,8 @@
     public function testPrepareLanguagesUnkown()
     {
         $languagesInput = ['foobar'];
-        $languagesOutput = CorpusFilterManager::prepareLanguages($languagesInput);
+        $filterManager = new CorpusFilterManager();
+        $languagesOutput = $filterManager->prepareLanguages($languagesInput);
         $this->assertEquals(['http://lexvo.org/id/iso639-3/foobar'], $languagesOutput);
     }
 
@@ -36,7 +38,8 @@
     public function testPrepareLanguagesAddPrefix()
     {
         $languagesInput = ['fra', 'gsw', 'bre', 'oci'];
-        $languagesOutput = CorpusFilterManager::prepareLanguages($languagesInput);
+        $filterManager = new CorpusFilterManager();
+        $languagesOutput = $filterManager->prepareLanguages($languagesInput);
         $this->assertEquals(['http://lexvo.org/id/iso639-3/fra', 'http://lexvo.org/id/iso639-3/gsw', 'http://lexvo.org/id/iso639-3/bre', 'http://lexvo.org/id/iso639-3/oci'], $languagesOutput);
     }
 
@@ -48,7 +51,8 @@
     public function testPrepareLanguagesMix()
     {
         $languagesInput = ['fra', 'http://lexvo.org/id/iso639-3/gsw', 'bre', 'http://lexvo.org/id/iso639-3/oci'];
-        $languagesOutput = CorpusFilterManager::prepareLanguages($languagesInput);
+        $filterManager = new CorpusFilterManager();
+        $languagesOutput = $filterManager->prepareLanguages($languagesInput);
         $this->assertEquals(['http://lexvo.org/id/iso639-3/fra', 'http://lexvo.org/id/iso639-3/gsw', 'http://lexvo.org/id/iso639-3/bre', 'http://lexvo.org/id/iso639-3/oci'], $languagesOutput);
     }
 
@@ -60,7 +64,8 @@
     public function testPrepareLanguagesRecusionSimple()
     {
         $languagesInput = ['http://lexvo.org/id/iso639-3/fra', 'http://lexvo.org/id/iso639-3/gsw', 'http://lexvo.org/id/iso639-3/bre', 'corpus-oil'];
-        $languagesOutput = CorpusFilterManager::prepareLanguages($languagesInput);
+        $filterManager = new CorpusFilterManager();
+        $languagesOutput = $filterManager->prepareLanguages($languagesInput);
         $this->assertEquals(['http://lexvo.org/id/iso639-3/fra', 'http://lexvo.org/id/iso639-3/gsw', 'http://lexvo.org/id/iso639-3/bre', 'http://lexvo.org/id/iso639-3/pcd'], $languagesOutput);
     }
 
@@ -72,7 +77,8 @@
     public function testPrepareLanguagesRecusionComplex()
     {
         $languagesInput = ['fra', 'http://lexvo.org/id/iso639-3/gsw', 'corpus-oil', 'corpus-regionals'];
-        $languagesOutput = CorpusFilterManager::prepareLanguages($languagesInput);
+        $filterManager = new CorpusFilterManager();
+        $languagesOutput = $filterManager->prepareLanguages($languagesInput);
         sort($languagesOutput);
         $languagesExpected = ['http://lexvo.org/id/iso639-3/fra', 'http://lexvo.org/id/iso639-3/gsw', 'http://lexvo.org/id/iso639-3/pcd',
              'http://lexvo.org/id/iso639-3/frp', 'http://lexvo.org/id/iso639-3/cos', 'http://lexvo.org/id/iso639-3/rcf',
@@ -89,7 +95,8 @@
     public function testPrepareLocationNoOp()
     {
         $locationInput = '3030293';
-        $locationOutput = CorpusFilterManager::prepareLocation($locationInput);
+        $filterManager = new CorpusFilterManager();
+        $locationOutput = $filterManager->prepareLocation($locationInput);
         $this->assertEquals('3030293', $locationOutput);
     }
 
@@ -101,7 +108,8 @@
     public function testPrepareLocationArray()
     {
         $locationInput = ['3030293', 'foobar'];
-        $locationOutput = CorpusFilterManager::prepareLocation($locationInput);
+        $filterManager = new CorpusFilterManager();
+        $locationOutput = $filterManager->prepareLocation($locationInput);
         $this->assertEquals('3030293', $locationOutput);
     }
 
@@ -114,7 +122,8 @@
     public function testPrepareLocationUnknown()
     {
         $locationInput = 'foobar';
-        $locationOutput = CorpusFilterManager::prepareLocation($locationInput);
+        $filterManager = new CorpusFilterManager();
+        $locationOutput = $filterManager->prepareLocation($locationInput);
         $this->assertEquals('foobar', $locationOutput);
     }
 
@@ -126,7 +135,8 @@
     public function testPrepareLocationGeonames()
     {
         $locationInput = 'http://sws.geonames.org/3030293';
-        $locationOutput = CorpusFilterManager::prepareLocation($locationInput);
+        $filterManager = new CorpusFilterManager();
+        $locationOutput = $filterManager->prepareLocation($locationInput);
         $this->assertEquals('3030293', $locationOutput);
     }
 
@@ -138,7 +148,8 @@
     public function testPrepareLocationGeonamesArray()
     {
         $locationInput = ['http://sws.geonames.org/3030293', 'http://www.geonames.org/3017382'];
-        $locationOutput = CorpusFilterManager::prepareLocation($locationInput);
+        $filterManager = new CorpusFilterManager();
+        $locationOutput = $filterManager->prepareLocation($locationInput);
         $this->assertEquals('3030293', $locationOutput);
     }
 
@@ -151,7 +162,8 @@
     public function testPrepareThemesNoOp()
     {
         $themesInput = ['ark:/12148/cb11937931x', 'ark:/12148/cb11946662b', 'ark:/12148/cb13318415c'];
-        $themesOutput = CorpusFilterManager::prepareTheme($themesInput);
+        $filterManager = new CorpusFilterManager();
+        $themesOutput = $filterManager->prepareTheme($themesInput);
         $this->assertEquals($themesInput, $themesOutput);
     }
 
@@ -163,7 +175,8 @@
     public function testPrepareThemesFullUrl()
     {
         $themesInput = ['http://ark.bnf.fr/ark:/12148/cb11937931x', 'http://data.bnf.fr/ark:/12148/cb11946662b', 'https://ark.bnf.fr/ark:/12148/cb13318415c'];
-        $themesOutput = CorpusFilterManager::prepareTheme($themesInput);
+        $filterManager = new CorpusFilterManager();
+        $themesOutput = $filterManager->prepareTheme($themesInput);
         $this->assertEquals(['ark:/12148/cb11937931x', 'ark:/12148/cb11946662b', 'ark:/12148/cb13318415c'], $themesOutput);
     }
 
@@ -176,7 +189,8 @@
     public function testPrepareThemesUnknown()
     {
         $themesInput = ['ark:/12148/cb11937931x', 'foo', 'ark:/12148/cb11946662b', 'ark:/12148/cb13318415c', 'bar'];
-        $themesOutput = CorpusFilterManager::prepareTheme($themesInput);
+        $filterManager = new CorpusFilterManager();
+        $themesOutput = $filterManager->prepareTheme($themesInput);
         $this->assertEquals(['ark:/12148/cb11937931x', 'ark:/12148/cb11946662b', 'ark:/12148/cb13318415c'], $themesOutput);
     }
 
@@ -188,7 +202,8 @@
     public function testPrepareThemesMix()
     {
         $themesInput = ['ark:/12148/cb11937931x', 'foo', 'http://data.bnf.fr/ark:/12148/cb11946662b', 'ark:/12148/cb13318415c', 'bar'];
-        $themesOutput = CorpusFilterManager::prepareTheme($themesInput);
+        $filterManager = new CorpusFilterManager();
+        $themesOutput = $filterManager->prepareTheme($themesInput);
         $this->assertEquals(['ark:/12148/cb11937931x', 'ark:/12148/cb11946662b', 'ark:/12148/cb13318415c'], $themesOutput);
     }
 
@@ -199,7 +214,8 @@
     public function testPrepareDate()
     {
         $datesInput = [ "1961", "1950-1960"];
-        $dateOutput = CorpusFilterManager::prepareDate($datesInput);
+        $filterManager = new CorpusFilterManager();
+        $dateOutput = $filterManager->prepareDate($datesInput);
         $this->assertEquals(['1950-1960', '1961'], $dateOutput);
     }
 
@@ -210,7 +226,8 @@
     public function testPrepareDateBadFormat()
     {
         $datesInput = [ "1961", "1950-1960", "foo"];
-        $dateOutput = CorpusFilterManager::prepareDate($datesInput);
+        $filterManager = new CorpusFilterManager();
+        $dateOutput = $filterManager->prepareDate($datesInput);
         $this->assertEquals(['1950-1960', '1961'], $dateOutput);
     }
 
@@ -222,7 +239,8 @@
     public function testPrepareDateNull()
     {
         $datesInput = null;
-        $dateOutput = CorpusFilterManager::prepareDate($datesInput);
+        $filterManager = new CorpusFilterManager();
+        $dateOutput = $filterManager->prepareDate($datesInput);
         $this->assertEquals([], $dateOutput);
     }
 
@@ -234,7 +252,8 @@
     public function testPrepareDateEmpty()
     {
         $datesInput = [];
-        $dateOutput = CorpusFilterManager::prepareDate($datesInput);
+        $filterManager = new CorpusFilterManager();
+        $dateOutput = $filterManager->prepareDate($datesInput);
         $this->assertEquals([], $dateOutput);
     }
 
--- a/server/src/tests/Libraries/Sparql/SparqlQueryAnalyserTest.php	Mon Oct 31 14:24:23 2016 +0100
+++ b/server/src/tests/Libraries/Sparql/SparqlQueryAnalyserTest.php	Thu Nov 03 01:52:26 2016 +0100
@@ -10,6 +10,7 @@
         "GRAPH" => "graph.sparql",
         "LIMIT_OFFSET" => "limit_offset.sparql",
         "PREFIXES" => "prefixes.sparql",
+        "PREFIXES_LIMIT" => "prefixes_limit.sparql",
         "SELECT" => "select.sparql",
         "UNKNOWN" => "unknown.sparql",
     ];
@@ -78,4 +79,22 @@
         ], $analyser->getRawPrefixes());
     }
 
+    public function testRawQuery() {
+        $analyser = new SparqlQueryAnalyser($this->getTestQuery("PREFIXES_LIMIT"));
+
+        $this->assertEquals(20, $analyser->getOffset());
+        $this->assertEquals(10, $analyser->getLimit());
+        $this->assertEquals("select  ?Nom ?resource ?url (count( distinct (?o) as ?nb))
+ where {
+   ?resource rdfs:label ?Nom.
+   ?resource foaf:isPrimaryTopicOf ?url.
+   ?resource rdf:type ?p.
+   ?resource dbpedia-owl:wikiPageExternalLink  ?o
+FILTER ( langMatches( lang(?Nom), \"EN\" )).
+?Nom <bif:contains> \"Apple\".
+MINUS { ?resource dbo:wikiPageRedirects|dbo:wikiPageDisambiguates ?dis   }
 }
+Group By ?Nom ?resource ?url", $analyser->getRawQuery());
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/src/tests/Libraries/Sparql/files/SparqlQueryAnalyserTest/prefixes_limit.sparql	Thu Nov 03 01:52:26 2016 +0100
@@ -0,0 +1,16 @@
+BASE <http://www.google.com/>
+PREFIX rdf:<http://www.w3.org/1999/02/22-rdf-syntax-ns#>
+PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+prefix foaf: <http://xmlns.com/foaf/0.1/>
+PREFIX dbpedia-owl: <http://dbpedia.org/ontology/>
+select  ?Nom ?resource ?url (count( distinct (?o) as ?nb))
+ where {
+   ?resource rdfs:label ?Nom.
+   ?resource foaf:isPrimaryTopicOf ?url.
+   ?resource rdf:type ?p.
+   ?resource dbpedia-owl:wikiPageExternalLink  ?o
+FILTER ( langMatches( lang(?Nom), "EN" )).
+?Nom <bif:contains> "Apple".
+MINUS { ?resource dbo:wikiPageRedirects|dbo:wikiPageDisambiguates ?dis   }
+}
+Group By ?Nom ?resource ?url LIMIT 10 OFFSET 20