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