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