1/*
2 * 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
17package androidx.room.verifier
18
19import androidx.room.processor.Context
20import androidx.room.vo.Entity
21import androidx.room.vo.Warning
22import columnInfo
23import org.sqlite.JDBC
24import java.io.File
25import java.sql.Connection
26import java.sql.DriverManager
27import java.sql.SQLException
28import java.util.UUID
29import java.util.regex.Pattern
30import 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 */
36class 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