1 /******************************************************************************
2  *
3  * Project:  MapServer
4  * Purpose:  MapCache tile caching support file: SQLite dimension support
5  * Author:   Thomas Bonfort and the MapServer team.
6  *
7  ******************************************************************************
8  * Copyright (c) 1996-2018 Regents of the University of Minnesota.
9  *
10  * Permission is hereby granted, free of charge, to any person obtaining a
11  * copy of this software and associated documentation files (the "Software"),
12  * to deal in the Software without restriction, including without limitation
13  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
14  * and/or sell copies of the Software, and to permit persons to whom the
15  * Software is furnished to do so, subject to the following conditions:
16  *
17  * The above copyright notice and this permission notice shall be included in
18  * all copies of this Software or works derived from this Software.
19  *
20  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21  * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
23  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26  * DEALINGS IN THE SOFTWARE.
27  *****************************************************************************/
28 
29 #include "mapcache.h"
30 #include <apr_strings.h>
31 #ifdef USE_SQLITE
32 #include <sqlite3.h>
33 #include <float.h>
34 
35 typedef struct mapcache_dimension_sqlite mapcache_dimension_sqlite;
36 
37 struct mapcache_dimension_sqlite {
38   mapcache_dimension dimension;
39   char *dbfile;
40   char *get_values_for_entry_query;
41   char *get_all_values_query;
42 };
43 
44 struct sqlite_dimension_conn {
45   sqlite3 *handle;
46   sqlite3_stmt **prepared_statements;
47   int n_statements;
48 };
49 
mapcache_sqlite_dimension_connection_constructor(mapcache_context * ctx,void ** conn_,void * params)50 void mapcache_sqlite_dimension_connection_constructor(mapcache_context *ctx, void **conn_, void *params)
51 {
52   int ret;
53   int flags;
54   char *dbfile = (char*) params;
55   struct sqlite_dimension_conn *conn = calloc(1, sizeof (struct sqlite_dimension_conn));
56   *conn_ = conn;
57   flags = SQLITE_OPEN_READONLY | SQLITE_OPEN_NOMUTEX;
58   ret = sqlite3_open_v2(dbfile, &conn->handle, flags, NULL);
59 
60   if (ret != SQLITE_OK) {
61     ctx->set_error(ctx,500,"failed to open sqlite dimension dbfile (%s): %s",dbfile,sqlite3_errmsg(conn->handle));
62     sqlite3_close(conn->handle);
63     *conn_=NULL;
64     return;
65   }
66   sqlite3_busy_timeout(conn->handle, 300000);
67 }
68 
mapcache_sqlite_dimension_connection_destructor(void * conn_)69 void mapcache_sqlite_dimension_connection_destructor(void *conn_)
70 {
71   struct sqlite_dimension_conn *conn = (struct sqlite_dimension_conn*) conn_;
72   while(conn->n_statements) {
73     conn->n_statements--;
74     if(conn->prepared_statements[conn->n_statements]) {
75       sqlite3_finalize(conn->prepared_statements[conn->n_statements]);
76     }
77   }
78   free(conn->prepared_statements);
79   sqlite3_close(conn->handle);
80   free(conn);
81 }
82 
_sqlite_dimension_get_conn(mapcache_context * ctx,mapcache_tileset * tileset,mapcache_dimension_sqlite * dim)83 static mapcache_pooled_connection* _sqlite_dimension_get_conn(mapcache_context *ctx, mapcache_tileset *tileset, mapcache_dimension_sqlite *dim) {
84   mapcache_dimension *pdim = (mapcache_dimension*)dim;
85   char *conn_key = apr_pstrcat(ctx->pool,"dim_",tileset?tileset->name:"","_",pdim->name,NULL);
86   mapcache_pooled_connection *pc = mapcache_connection_pool_get_connection(ctx,conn_key,
87         mapcache_sqlite_dimension_connection_constructor,
88         mapcache_sqlite_dimension_connection_destructor, dim->dbfile);
89   return pc;
90 }
91 
_sqlite_dimension_release_conn(mapcache_context * ctx,mapcache_pooled_connection * pc)92 static void _sqlite_dimension_release_conn(mapcache_context *ctx, mapcache_pooled_connection *pc)
93 {
94   if(GC_HAS_ERROR(ctx)) {
95     mapcache_connection_pool_invalidate_connection(ctx,pc);
96   } else {
97     mapcache_connection_pool_release_connection(ctx,pc);
98   }
99 }
100 
_mapcache_dimension_sqlite_bind_parameters(mapcache_context * ctx,sqlite3_stmt * stmt,sqlite3 * handle,const char * value,mapcache_tileset * tileset,mapcache_extent * extent,mapcache_grid * grid)101 static void _mapcache_dimension_sqlite_bind_parameters(mapcache_context *ctx, sqlite3_stmt *stmt, sqlite3 *handle,
102                                                        const char *value,
103                                                        mapcache_tileset *tileset, mapcache_extent *extent, mapcache_grid *grid) {
104   int paramidx,ret;
105   paramidx = sqlite3_bind_parameter_index(stmt, ":dim");
106   if (paramidx) {
107     ret = sqlite3_bind_text(stmt, paramidx, value, -1, SQLITE_STATIC);
108     if(ret != SQLITE_OK) {
109       ctx->set_error(ctx,400, "sqlite dimension failed to bind :dim : %s", sqlite3_errmsg(handle));
110       return;
111     }
112   }
113 
114   if(tileset) {
115     paramidx = sqlite3_bind_parameter_index(stmt, ":tileset");
116     if (paramidx) {
117       ret = sqlite3_bind_text(stmt, paramidx, tileset->name, -1, SQLITE_STATIC);
118       if(ret != SQLITE_OK) {
119         ctx->set_error(ctx,400, "sqlite dimension failed to bind :tileset : %s", sqlite3_errmsg(handle));
120         return;
121       }
122     }
123   }
124 
125   if(grid) {
126     paramidx = sqlite3_bind_parameter_index(stmt, ":gridsrs");
127     if (paramidx) {
128       ret = sqlite3_bind_text(stmt, paramidx, grid->srs, -1, SQLITE_STATIC);
129       if(ret != SQLITE_OK) {
130         ctx->set_error(ctx,400, "failed to bind :gridsrs %s", sqlite3_errmsg(handle));
131         return;
132       }
133     }
134   }
135 
136   paramidx = sqlite3_bind_parameter_index(stmt, ":minx");
137   if (paramidx) {
138     ret = sqlite3_bind_double(stmt, paramidx, extent?extent->minx:-DBL_MAX);
139     if(ret != SQLITE_OK) {
140       ctx->set_error(ctx,400, "failed to bind :minx %s", sqlite3_errmsg(handle));
141       return;
142     }
143   }
144   paramidx = sqlite3_bind_parameter_index(stmt, ":miny");
145   if (paramidx) {
146     ret = sqlite3_bind_double(stmt, paramidx, extent?extent->miny:-DBL_MAX);
147     if(ret != SQLITE_OK) {
148       ctx->set_error(ctx,400, "failed to bind :miny %s", sqlite3_errmsg(handle));
149       return;
150     }
151   }
152   paramidx = sqlite3_bind_parameter_index(stmt, ":maxx");
153   if (paramidx) {
154     ret = sqlite3_bind_double(stmt, paramidx, extent?extent->maxx:DBL_MAX);
155     if(ret != SQLITE_OK) {
156       ctx->set_error(ctx,400, "failed to bind :maxx %s", sqlite3_errmsg(handle));
157       return;
158     }
159   }
160   paramidx = sqlite3_bind_parameter_index(stmt, ":maxy");
161   if (paramidx) {
162     ret = sqlite3_bind_double(stmt, paramidx, extent?extent->maxy:DBL_MAX);
163     if(ret != SQLITE_OK) {
164       ctx->set_error(ctx,400, "failed to bind :maxy %s", sqlite3_errmsg(handle));
165       return;
166     }
167   }
168 }
_mapcache_dimension_sqlite_get_entries_for_value(mapcache_context * ctx,mapcache_dimension * dim,const char * value,mapcache_tileset * tileset,mapcache_extent * extent,mapcache_grid * grid)169 static apr_array_header_t* _mapcache_dimension_sqlite_get_entries_for_value(mapcache_context *ctx, mapcache_dimension *dim, const char *value,
170                                                                            mapcache_tileset *tileset, mapcache_extent *extent, mapcache_grid *grid)
171 {
172   mapcache_dimension_sqlite *dimension = (mapcache_dimension_sqlite*)dim;
173   struct sqlite_dimension_conn *conn = NULL;
174   mapcache_pooled_connection *pc;
175   apr_array_header_t *values = apr_array_make(ctx->pool,1,sizeof(char*));
176   int sqliteret;
177   pc = _sqlite_dimension_get_conn(ctx,tileset,dimension);
178   if (GC_HAS_ERROR(ctx)) {
179     return values;
180   }
181   conn = pc->connection;
182   if(!conn->prepared_statements) {
183     conn->prepared_statements = calloc(2,sizeof(sqlite3_stmt*));
184     conn->n_statements = 2;
185   }
186   if(!conn->prepared_statements[0]) {
187     if(SQLITE_OK != sqlite3_prepare_v2(conn->handle, dimension->get_values_for_entry_query, -1, &conn->prepared_statements[0], NULL)) {
188       ctx->set_error(ctx, 500, "sqlite dimension backend failed on preparing query: %s", sqlite3_errmsg(conn->handle));
189       goto cleanup;
190     }
191   }
192 
193   _mapcache_dimension_sqlite_bind_parameters(ctx,conn->prepared_statements[0],conn->handle,value,
194       tileset,extent,grid);
195   if (GC_HAS_ERROR(ctx)) {
196     goto cleanup;
197   }
198 
199 
200   do {
201     sqliteret = sqlite3_step(conn->prepared_statements[0]);
202     if (sqliteret != SQLITE_DONE && sqliteret != SQLITE_ROW && sqliteret != SQLITE_BUSY && sqliteret != SQLITE_LOCKED) {
203       ctx->set_error(ctx, 500, "sqlite dimension backend failed on query : %s (%d)", sqlite3_errmsg(conn->handle), sqliteret);
204       goto cleanup;
205     }
206     if(sqliteret == SQLITE_ROW) {
207       const char* dimrow = (const char*) sqlite3_column_text(conn->prepared_statements[0], 0);
208       APR_ARRAY_PUSH(values,char*) = apr_pstrdup(ctx->pool,dimrow);
209     }
210   } while (sqliteret == SQLITE_ROW || sqliteret == SQLITE_BUSY || sqliteret == SQLITE_LOCKED);
211 
212 cleanup:
213   if(conn->prepared_statements[0]) {
214     sqlite3_reset(conn->prepared_statements[0]);
215   }
216   _sqlite_dimension_release_conn(ctx,pc);
217 
218   return values;
219 }
220 
_mapcache_dimension_sqlite_get_all_entries(mapcache_context * ctx,mapcache_dimension * dim,mapcache_tileset * tileset,mapcache_extent * extent,mapcache_grid * grid)221 static apr_array_header_t* _mapcache_dimension_sqlite_get_all_entries(mapcache_context *ctx, mapcache_dimension *dim,
222                                 mapcache_tileset *tileset, mapcache_extent *extent, mapcache_grid *grid)
223 {
224   mapcache_dimension_sqlite *dimension = (mapcache_dimension_sqlite*)dim;
225   struct sqlite_dimension_conn *conn = NULL;
226   int sqliteret;
227   apr_array_header_t *ret = apr_array_make(ctx->pool,0,sizeof(char*));
228   mapcache_pooled_connection *pc;
229   pc = _sqlite_dimension_get_conn(ctx,tileset,dimension);
230   if (GC_HAS_ERROR(ctx)) {
231     goto cleanup;
232   }
233   conn = pc->connection;
234   if(!conn->prepared_statements) {
235     conn->prepared_statements = calloc(2,sizeof(sqlite3_stmt*));
236     conn->n_statements = 2;
237   }
238 
239   if(!conn->prepared_statements[1]) {
240     sqliteret = sqlite3_prepare_v2(conn->handle, dimension->get_all_values_query, -1, &conn->prepared_statements[1], NULL);
241     if(sqliteret != SQLITE_OK) {
242       ctx->set_error(ctx, 500, "sqlite dimension backend failed on preparing query: %s", sqlite3_errmsg(conn->handle));
243       goto cleanup;
244     }
245   }
246   _mapcache_dimension_sqlite_bind_parameters(ctx,conn->prepared_statements[1],conn->handle,NULL,tileset,extent,grid);
247   if (GC_HAS_ERROR(ctx)) {
248     return ret;
249   }
250   do {
251     sqliteret = sqlite3_step(conn->prepared_statements[1]);
252     if (sqliteret != SQLITE_DONE && sqliteret != SQLITE_ROW && sqliteret != SQLITE_BUSY && sqliteret != SQLITE_LOCKED) {
253       ctx->set_error(ctx, 500, "sqlite dimension backend failed on query : %s (%d)", sqlite3_errmsg(conn->handle), sqliteret);
254       goto cleanup;
255     }
256     if(sqliteret == SQLITE_ROW) {
257       const char* sqdim = (const char*) sqlite3_column_text(conn->prepared_statements[1], 0);
258       APR_ARRAY_PUSH(ret,char*) = apr_pstrdup(ctx->pool,sqdim);
259     }
260   } while (sqliteret == SQLITE_ROW || sqliteret == SQLITE_BUSY || sqliteret == SQLITE_LOCKED);
261 
262 cleanup:
263   if(conn->prepared_statements[1]) {
264     sqlite3_reset(conn->prepared_statements[1]);
265   }
266   _sqlite_dimension_release_conn(ctx,pc);
267 
268   return ret;
269 }
270 
271 
_mapcache_dimension_sqlite_parse_xml(mapcache_context * ctx,mapcache_dimension * dim,ezxml_t node)272 static void _mapcache_dimension_sqlite_parse_xml(mapcache_context *ctx, mapcache_dimension *dim,
273     ezxml_t node)
274 {
275   mapcache_dimension_sqlite *dimension;
276   ezxml_t child;
277 
278   dimension = (mapcache_dimension_sqlite*)dim;
279 
280   child = ezxml_child(node,"dbfile");
281   if(child) {
282     dimension->dbfile = apr_pstrdup(ctx->pool, child->txt);
283   } else {
284     ctx->set_error(ctx,400,"sqlite dimension \"%s\" has no <dbfile> node", dim->name);
285     return;
286   }
287   child = ezxml_child(node,"validate_query");
288   if(child) {
289     dimension->get_values_for_entry_query = apr_pstrdup(ctx->pool, child->txt);
290   } else {
291     ctx->set_error(ctx,400,"sqlite dimension \"%s\" has no <validate_query> node", dim->name);
292     return;
293   }
294   child = ezxml_child(node,"list_query");
295   if(child) {
296     dimension->get_all_values_query = apr_pstrdup(ctx->pool, child->txt);
297   } else {
298     ctx->set_error(ctx,400,"sqlite dimension \"%s\" has no <list_query> node", dim->name);
299     return;
300   }
301 
302 }
303 
304 
_bind_sqlite_dimension_time_params(mapcache_context * ctx,sqlite3_stmt * stmt,sqlite3 * handle,const char * dim_value,mapcache_tileset * tileset,mapcache_grid * grid,mapcache_extent * extent,time_t start,time_t end)305 static void _bind_sqlite_dimension_time_params(mapcache_context *ctx, sqlite3_stmt *stmt,
306         sqlite3 *handle, const char *dim_value, mapcache_tileset *tileset, mapcache_grid *grid, mapcache_extent *extent,
307         time_t start, time_t end)
308 {
309   int paramidx,ret;
310 
311   _mapcache_dimension_sqlite_bind_parameters(ctx,stmt,handle,dim_value,tileset,extent,grid);
312 
313   paramidx = sqlite3_bind_parameter_index(stmt, ":start_timestamp");
314   if (paramidx) {
315     ret = sqlite3_bind_int64(stmt, paramidx, start);
316     if(ret != SQLITE_OK) {
317       ctx->set_error(ctx,400, "failed to bind :start_timestamp: %s", sqlite3_errmsg(handle));
318       return;
319     }
320   }
321 
322   paramidx = sqlite3_bind_parameter_index(stmt, ":end_timestamp");
323   if (paramidx) {
324     ret = sqlite3_bind_int64(stmt, paramidx, end);
325     if(ret != SQLITE_OK) {
326       ctx->set_error(ctx,400, "failed to bind :end_timestamp: %s", sqlite3_errmsg(handle));
327       return;
328     }
329   }
330 }
331 
_mapcache_dimension_sqlite_get_entries_for_time_range(mapcache_context * ctx,mapcache_dimension * dim,const char * dim_value,time_t start,time_t end,mapcache_tileset * tileset,mapcache_extent * extent,mapcache_grid * grid)332 apr_array_header_t* _mapcache_dimension_sqlite_get_entries_for_time_range(mapcache_context *ctx, mapcache_dimension *dim, const char *dim_value,
333         time_t start, time_t end, mapcache_tileset *tileset, mapcache_extent *extent, mapcache_grid *grid) {
334   mapcache_dimension_sqlite *sdim = (mapcache_dimension_sqlite*)dim;
335   int ret;
336   apr_array_header_t *time_ids = NULL;
337   mapcache_pooled_connection *pc;
338   struct sqlite_dimension_conn *conn;
339   pc = _sqlite_dimension_get_conn(ctx,tileset,sdim);
340   if (GC_HAS_ERROR(ctx)) {
341     return NULL;
342   }
343   conn = pc->connection;
344   if(!conn->prepared_statements) {
345     conn->prepared_statements = calloc(1,sizeof(sqlite3_stmt*));
346     conn->n_statements = 1;
347   }
348   if(!conn->prepared_statements[0]) {
349     ret = sqlite3_prepare_v2(conn->handle, sdim->get_values_for_entry_query, -1, &conn->prepared_statements[0], NULL);
350     if(ret != SQLITE_OK) {
351       ctx->set_error(ctx, 500, "time sqlite backend failed on preparing query: %s", sqlite3_errmsg(conn->handle));
352       _sqlite_dimension_release_conn(ctx, pc);
353       return NULL;
354     }
355   }
356 
357   _bind_sqlite_dimension_time_params(ctx,conn->prepared_statements[0],conn->handle,dim_value,tileset,grid,extent,start,end);
358   if(GC_HAS_ERROR(ctx)) {
359     _sqlite_dimension_release_conn(ctx, pc);
360     return NULL;
361   }
362 
363   time_ids = apr_array_make(ctx->pool,0,sizeof(char*));
364   do {
365     ret = sqlite3_step(conn->prepared_statements[0]);
366     if (ret != SQLITE_DONE && ret != SQLITE_ROW && ret != SQLITE_BUSY && ret != SQLITE_LOCKED) {
367       ctx->set_error(ctx, 500, "sqlite backend failed on dimension_time query : %s (%d)", sqlite3_errmsg(conn->handle), ret);
368       _sqlite_dimension_release_conn(ctx, pc);
369       return NULL;
370     }
371     if (ret == SQLITE_ROW) {
372       const char *time_id = (const char *)sqlite3_column_text(conn->prepared_statements[0], 0);
373       APR_ARRAY_PUSH(time_ids, char *) = apr_pstrdup(ctx->pool, time_id);
374     }
375   } while (ret == SQLITE_ROW || ret == SQLITE_BUSY || ret == SQLITE_LOCKED);
376 
377   sqlite3_reset(conn->prepared_statements[0]);
378   _sqlite_dimension_release_conn(ctx, pc);
379   return time_ids;
380 }
381 
382 /*
383 apr_array_header_t* _mapcache_dimension_time_get_all_entries(mapcache_context *ctx, mapcache_dimension *dim,
384         mapcache_tileset *tileset, mapcache_extent *extent, mapcache_grid *grid) {
385   mapcache_dimension_time *tdim = (mapcache_dimension_time*)dim;
386   time_t all[2] = {0,INT_MAX};
387   return _mapcache_dimension_time_get_entries(ctx,tdim,NULL,tileset,extent,grid,all,1);
388 }
389 */
390 #endif
391 
mapcache_dimension_sqlite_create(mapcache_context * ctx,apr_pool_t * pool)392 mapcache_dimension* mapcache_dimension_sqlite_create(mapcache_context *ctx, apr_pool_t *pool)
393 {
394 #ifdef USE_SQLITE
395   mapcache_dimension_sqlite *dimension = apr_pcalloc(pool, sizeof(mapcache_dimension_sqlite));
396   dimension->dimension.type = MAPCACHE_DIMENSION_SQLITE;
397   dimension->dbfile = NULL;
398   dimension->dimension._get_entries_for_value = _mapcache_dimension_sqlite_get_entries_for_value;
399   dimension->dimension._get_entries_for_time_range = _mapcache_dimension_sqlite_get_entries_for_time_range;
400   dimension->dimension.configuration_parse_xml = _mapcache_dimension_sqlite_parse_xml;
401   dimension->dimension.get_all_entries = _mapcache_dimension_sqlite_get_all_entries;
402   dimension->dimension.get_all_ogc_formatted_entries = _mapcache_dimension_sqlite_get_all_entries;
403   return (mapcache_dimension*)dimension;
404 #else
405   ctx->set_error(ctx,400,"Sqlite dimension support requires SQLITE support to be built in");
406   return NULL;
407 #endif
408 }
409