1 /*
<lambda>null2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * 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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.room.verifier
18 
19 import androidx.room.processor.Context
20 import androidx.room.vo.Entity
21 import androidx.room.vo.Warning
22 import columnInfo
23 import org.sqlite.JDBC
24 import java.io.File
25 import java.sql.Connection
26 import java.sql.DriverManager
27 import java.sql.SQLException
28 import java.util.UUID
29 import java.util.regex.Pattern
30 import javax.lang.model.element.Element
31 
32 /**
33  * Builds an in-memory version of the database and verifies the queries against it.
34  * This class is also used to resolve the return types.
35  */
36 class DatabaseVerifier private constructor(
37         val connection: Connection, val context: Context, val entities: List<Entity>) {
38     companion object {
39         private const val CONNECTION_URL = "jdbc:sqlite::memory:"
40         /**
41          * Taken from:
42          * https://github.com/robolectric/robolectric/blob/master/shadows/framework/
43          * src/main/java/org/robolectric/shadows/ShadowSQLiteConnection.java#L94
44          *
45          * This is actually not accurate because it might swap anything since it does not parse
46          * SQL. That being said, for the verification purposes, it does not matter and clearly
47          * much easier than parsing and rebuilding the query.
48          */
49         private val COLLATE_LOCALIZED_UNICODE_PATTERN = Pattern.compile(
50                 "\\s+COLLATE\\s+(LOCALIZED|UNICODE)", Pattern.CASE_INSENSITIVE)
51 
52         init {
53             // see: https://github.com/xerial/sqlite-jdbc/issues/97
54             val tmpDir = System.getProperty("java.io.tmpdir")
55             if (tmpDir != null) {
56                 val outDir = File(tmpDir, "room-${UUID.randomUUID()}")
57                 outDir.mkdirs()
58                 outDir.deleteOnExit()
59                 System.setProperty("org.sqlite.tmpdir", outDir.absolutePath)
60                 // dummy call to trigger JDBC initialization so that we can unregister it
61                 JDBC.isValidURL(CONNECTION_URL)
62                 unregisterDrivers()
63             }
64         }
65 
66         /**
67          * Tries to create a verifier but returns null if it cannot find the driver.
68          */
69         fun create(context: Context, element: Element, entities: List<Entity>): DatabaseVerifier? {
70             return try {
71                 val connection = JDBC.createConnection(CONNECTION_URL, java.util.Properties())
72                 DatabaseVerifier(connection, context, entities)
73             } catch (ex: Exception) {
74                 context.logger.w(Warning.CANNOT_CREATE_VERIFICATION_DATABASE, element,
75                         DatabaseVerificaitonErrors.cannotCreateConnection(ex))
76                 null
77             }
78         }
79 
80         /**
81          * Unregisters the JDBC driver. If we don't do this, we'll leak the driver which leaks a
82          * whole class loader.
83          * see: https://github.com/xerial/sqlite-jdbc/issues/267
84          * see: https://issuetracker.google.com/issues/62473121
85          */
86         private fun unregisterDrivers() {
87             try {
88                 DriverManager.getDriver(CONNECTION_URL)?.let {
89                     DriverManager.deregisterDriver(it)
90                 }
91             } catch (t: Throwable) {
92                 System.err.println("Room: cannot unregister driver ${t.message}")
93             }
94         }
95     }
96     init {
97         entities.forEach { entity ->
98             val stmt = connection.createStatement()
99             stmt.executeUpdate(stripLocalizeCollations(entity.createTableQuery))
100         }
101     }
102 
103     fun analyze(sql: String): QueryResultInfo {
104         return try {
105             val stmt = connection.prepareStatement(stripLocalizeCollations(sql))
106             QueryResultInfo(stmt.columnInfo())
107         } catch (ex: SQLException) {
108             QueryResultInfo(emptyList(), ex)
109         }
110     }
111 
112     private fun stripLocalizeCollations(sql: String) =
113         COLLATE_LOCALIZED_UNICODE_PATTERN.matcher(sql).replaceAll(" COLLATE NOCASE")
114 
115     fun closeConnection(context: Context) {
116         if (!connection.isClosed) {
117             try {
118                 connection.close()
119             } catch (t: Throwable) {
120                 //ignore.
121                 context.logger.d("failed to close the database connection ${t.message}")
122             }
123         }
124     }
125 }
126