/** * Copyright (C) 2013 Johannes Schnatterer * * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This file is part of nusic. * * nusic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * nusic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with nusic. If not, see <http://www.gnu.org/licenses/>. */ package info.schnatterer.nusic.data; import info.schnatterer.nusic.data.model.Artist; import info.schnatterer.nusic.data.model.Release; import info.schnatterer.nusic.data.util.SqliteUtil; import info.schnatterer.nusic.util.DateUtil; import javax.inject.Inject; import javax.inject.Provider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import roboguice.inject.ContextSingleton; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.provider.BaseColumns; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * Basic database abstraction. Handles execution of DDL scripts. * * @author schnatterer * */ @ContextSingleton public class NusicDatabaseSqlite extends SQLiteOpenHelper { private static final Logger LOG = LoggerFactory .getLogger(NusicDatabaseSqlite.class); private static final String DATABASE_NAME = "nusic"; /** Last app version that needed a database update. */ private static final int DATABASE_VERSION = SqliteDatabaseVersion.V6; private static final String DATABASE_TABLE_CREATE = "CREATE TABLE "; private static final String DATABASE_TABLE_DROP = "DROP TABLE "; private static final String DATABASE_FOREIGN_KEY = "FOREIGN KEY("; private static final String DATABASE_REFERENCES = "REFERENCES "; @Inject private static Provider<Context> contextProvider; public NusicDatabaseSqlite() { super(contextProvider.get(), DATABASE_NAME, null, DATABASE_VERSION); } @Override protected void finalize() throws Throwable { close(); super.finalize(); } @Override public synchronized void close() { LOG.debug("Closing database"); super.close(); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(TableArtist.create()); db.execSQL(TableRelease.create()); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion == SqliteDatabaseVersion.V1) { /* * In v.2.1: Column releases.TYPE_COLUMN_RELEASEMB_ID now * corresponds to MusicBrainz's "release group ID" instead of the * "release id". In order to make sure to avoid inconsistencies and * because a real migration is way too much effort at this early * point, we're just going for a clean slate. */ db.execSQL(DATABASE_TABLE_DROP + TableRelease.NAME); db.execSQL(TableRelease.create()); } if (oldVersion < SqliteDatabaseVersion.V4) { // Just append new columns, they can be null db.execSQL(addColumn(TableArtist.NAME, TableArtist.COLUMN_IS_HIDDEN, TableArtist.TYPE_COLUMN_IS_HIDDEN)); db.execSQL(addColumn(TableRelease.NAME, TableRelease.COLUMN_IS_HIDDEN, TableRelease.TYPE_COLUMN_IS_HIDDEN)); } if (oldVersion < SqliteDatabaseVersion.V5) { /* * Set all release dates to null. They are filled again on app * start. */ ContentValues contentValues = new ContentValues(); contentValues .put(TableRelease.COLUMN_DATE_RELEASED, (Integer) null); db.update(TableRelease.NAME, contentValues, null, null); } if (oldVersion < SqliteDatabaseVersion.V6) { db.execSQL(addColumn(TableRelease.NAME, TableRelease.COLUMN_COVERARTARCHIVE_ID, TableRelease.TYPE_COLUMN_COVERARTARCHIVE_ID)); } // When changing the database, don't forget to create a new version } /** * Adds a column at the end of a table. * * @param tableName * table where to add the new column * @param columnName * new column name * @param columnType * type of new column * * @return the SQL string for appending a column to a table */ private String addColumn(String tableName, String columnName, String columnType) { return new StringBuffer("ALTER TABLE ").append(tableName) .append(" ADD ").append(columnName).append(" ") .append(columnType).toString(); } /** * Facilitates creation of a createTable String * * @param tableName * the table name * @param columnsAndTypeTuples * a multiple of two (at least two) containing the column name * and the type (can contain additional constraints) * @return an SQL string that creates the table */ static String createTable(String tableName, String... columnsAndTypeTuples) { StringBuffer sql = new StringBuffer(DATABASE_TABLE_CREATE).append( tableName).append("("); int index = 0; sql.append(columnsAndTypeTuples[index++]).append(" ") .append(columnsAndTypeTuples[index++]); while (index < columnsAndTypeTuples.length) { sql.append(", "); sql.append(columnsAndTypeTuples[index++]).append(" ") .append(columnsAndTypeTuples[index++]); } sql.append(");"); return sql.toString(); } public static interface SqliteDatabaseVersion { /** * Very first release of this app */ int V1 = 1; /** * Update DB schema because release.musicbrainzId is now stores the * releaseGroupId. */ int V3 = 3; /** * Added Artist.isHidden and Release.isHidden */ int V4 = 4; /** * Reset {@link TableRelease#COLUMN_DATE_RELEASED} due to invalid values * (no UTC values) */ int V5 = 5; /** Added column for release cover art archive ID. */ int V6 = 6; } /** * Definition and basic mappings for table corresponding to {@link Release} * entity. * * @author schnatterer * */ public static class TableRelease { public static final String NAME = "release"; public static final String COLUMN_ID = BaseColumns._ID; public static final String TYPE_COLUMN_ID = "INTEGER PRIMARY KEY AUTOINCREMENT"; public static final int INDEX_COLUMN_RELEASEID = 0; public static final String COLUMN_MB_ID = "mbId"; public static final String TYPE_COLUMN_MB_ID = "INTEGER"; public static final int INDEX_COLUMN_MB_ID = 1; public static final String COLUMN_NAME = "name"; public static final String TYPE_COLUMN_RELEASENAME = "TEXT NOT NULL"; public static final int INDEX_COLUMN_RELEASENAME = 2; public static final String COLUMN_DATE_RELEASED = "dateReleased"; public static final String TYPE_COLUMN_RELEASEDATE_RELEASED = "INTEGER"; public static final int INDEX_COLUMN_RELEASEDATE_RELEASED = 3; public static final String COLUMN_DATE_CREATED = "dateCreated"; public static final String TYPE_COLUMN_RELEASEDATE_CREATED = "INTEGER NOT NULL"; public static final int INDEX_COLUMN_RELEASEDATE_CREATED = 4; public static final String COLUMN_RELEASEARTWORK_PATH = "artworkPath"; public static final String TYPE_COLUMN_RELEASEARTWORK_PATH = "TEXT"; public static final int INDEX_COLUMN_RELEASEARTWORK_PATH = 5; public static final String COLUMN_FK_ID_ARTIST = "fkIdArtist"; public static final String TYPE_COLUMN_FK_ID_ARTIST = "INTEGER"; public static final String TYPE_COLUMN_FK_ID_CONSTRAINT_FK = DATABASE_FOREIGN_KEY + COLUMN_FK_ID_ARTIST + ")"; public static final String TYPE_COLUMN_FK_ID_CONSTRAINT_REFERENCES = DATABASE_REFERENCES + NAME + "(" + COLUMN_ID + ")"; public static final int INDEX_COLUMN_FK_ID_ARTIST = 6; public static final String COLUMN_IS_HIDDEN = "isHidden"; public static final String TYPE_COLUMN_IS_HIDDEN = "INTEGER"; public static final int INDEX_COLUMN_IS_HIDDEN = 7; public static final String COLUMN_COVERARTARCHIVE_ID = "coverArtArchiveId"; public static final String TYPE_COLUMN_COVERARTARCHIVE_ID = "INTEGER"; public static final int INDEX_COLUMN_COVERARTARCHIVE_ID = 8; public static final List<String> COLUMNS = Collections.unmodifiableList( Arrays.asList(COLUMN_ID, COLUMN_MB_ID, COLUMN_NAME, COLUMN_DATE_RELEASED, COLUMN_DATE_CREATED, COLUMN_RELEASEARTWORK_PATH, COLUMN_FK_ID_ARTIST, COLUMN_IS_HIDDEN, COLUMN_COVERARTARCHIVE_ID)); public static final String COLUMNS_ALL = new StringBuilder(NAME) .append(".").append(COLUMN_ID).append(",").append(NAME) .append(".").append(COLUMN_MB_ID).append(",").append(NAME) .append(".").append(COLUMN_NAME).append(",").append(NAME) .append(".").append(COLUMN_DATE_RELEASED).append(",") .append(NAME).append(".").append(COLUMN_DATE_CREATED) .append(",").append(NAME).append(".") .append(COLUMN_RELEASEARTWORK_PATH).append(",").append(NAME) .append(".").append(COLUMN_FK_ID_ARTIST).append(",") .append(NAME).append(".").append(COLUMN_IS_HIDDEN).append(",") .append(NAME).append(".").append(COLUMN_COVERARTARCHIVE_ID) .toString(); public static Long toId(Cursor cursor, int startIndex) { return cursor.getLong(startIndex + INDEX_COLUMN_RELEASEID); } public static Release toEntity(Cursor cursor, int startIndex) { Release release = new Release(SqliteUtil.loadDate(cursor, startIndex + INDEX_COLUMN_RELEASEDATE_CREATED)); release.setId(toId(cursor, startIndex)); // release.setArtworkPath(cursor.getString(startIndex // + INDEX_COLUMN_RELEASEARTWORK_PATH)); release.setMusicBrainzId(cursor.getString(startIndex + INDEX_COLUMN_MB_ID)); release.setReleaseDate(SqliteUtil.loadDate(cursor, startIndex + INDEX_COLUMN_RELEASEDATE_RELEASED)); release.setReleaseName(cursor.getString(startIndex + INDEX_COLUMN_RELEASENAME)); release.setHidden(SqliteUtil.loadBoolean(cursor, INDEX_COLUMN_IS_HIDDEN)); release.setCoverartArchiveId(cursor .getLong(INDEX_COLUMN_COVERARTARCHIVE_ID)); return release; } public static ContentValues toContentValues(Release release) { ContentValues values = new ContentValues(); SqliteUtil.putIfNotNull(values, COLUMN_MB_ID, release.getMusicBrainzId()); SqliteUtil.putIfNotNull(values, COLUMN_NAME, release.getReleaseName()); SqliteUtil.putIfNotNull(values, COLUMN_DATE_RELEASED, DateUtil.toLong(release.getReleaseDate())); SqliteUtil.putIfNotNull(values, COLUMN_DATE_CREATED, DateUtil.toLong(release.getDateCreated())); // SqliteUtil.putIfNotNull(values, COLUMN_RELEASEARTWORK_PATH, // release.getArtworkPath()); SqliteUtil.putIfNotNull(values, COLUMN_FK_ID_ARTIST, release .getArtist().getId()); SqliteUtil.putIfNotNull(values, COLUMN_IS_HIDDEN, release.isHidden()); SqliteUtil.putIfNotNull(values, COLUMN_COVERARTARCHIVE_ID, release.getCoverartArchiveId()); return values; } public static String create() { return createTable(NAME, COLUMN_ID, TYPE_COLUMN_ID, COLUMN_MB_ID, TYPE_COLUMN_MB_ID, COLUMN_NAME, TYPE_COLUMN_RELEASENAME, COLUMN_DATE_RELEASED, TYPE_COLUMN_RELEASEDATE_RELEASED, COLUMN_DATE_CREATED, TYPE_COLUMN_RELEASEDATE_CREATED, COLUMN_RELEASEARTWORK_PATH, TYPE_COLUMN_RELEASEARTWORK_PATH, COLUMN_FK_ID_ARTIST, TYPE_COLUMN_FK_ID_ARTIST, COLUMN_IS_HIDDEN, TYPE_COLUMN_IS_HIDDEN, COLUMN_COVERARTARCHIVE_ID, TYPE_COLUMN_COVERARTARCHIVE_ID, // Constraints TYPE_COLUMN_FK_ID_CONSTRAINT_FK, TYPE_COLUMN_FK_ID_CONSTRAINT_REFERENCES); } } /** * Definition and basic mappings for table corresponding to {@link Artist} * entity. * * @author schnatterer * */ public static class TableArtist { public static final String NAME = "artist"; public static final String COLUMN_ID = BaseColumns._ID; public static final String TYPE_COLUMN_ID = "INTEGER PRIMARY KEY AUTOINCREMENT"; public static final int INDEX_COLUMN_ID = 0; public static final String COLUMN_ANDROID_ID = "androidId"; public static final String TYPE_COLUMN_ANDROID_ID = "INTEGER"; public static final int INDEX_COLUMN_ANDROID_ID = 1; public static final String COLUMN_MB_ID = "mbId"; public static final String TYPE_COLUMN_MB_ID = "STRING"; public static final int INDEX_COLUMN_MB_ID = 2; public static final String COLUMN_NAME = "name"; public static final String TYPE_COLUMN_NAME = "TEXT NOT NULL"; public static final int INDEX_COLUMN_NAME = 3; public static final String COLUMN_DATE_CREATED = "dateCreated"; public static final String TYPE_COLUMN_DATE_CREATED = "INTEGER NOT NULL"; public static final int INDEX_COLUMN_DATE_CREATED = 4; public static final String COLUMN_IS_HIDDEN = "isHidden"; public static final String TYPE_COLUMN_IS_HIDDEN = "INTEGER"; public static final int INDEX_COLUMN_IS_HIDDEN = 5; public static final String COLUMNS_ALL = new StringBuilder(NAME) .append(".").append(COLUMN_ID).append(",").append(NAME) .append(".").append(COLUMN_ANDROID_ID).append(",").append(NAME) .append(".").append(COLUMN_MB_ID).append(",").append(NAME) .append(".").append(COLUMN_NAME).append(",").append(NAME) .append(".").append(COLUMN_DATE_CREATED).append(",") .append(NAME).append(".").append(COLUMN_IS_HIDDEN).toString(); public static Long toId(Cursor cursor, int startIndex) { return cursor.getLong(startIndex + INDEX_COLUMN_ID); } public static Artist toEntity(Cursor cursor, int startIndex) { Artist artist = new Artist(SqliteUtil.loadDate(cursor, startIndex + INDEX_COLUMN_DATE_CREATED)); artist.setId(toId(cursor, startIndex)); artist.setAndroidAudioArtistId(cursor.getLong(startIndex + INDEX_COLUMN_ANDROID_ID)); artist.setMusicBrainzId(cursor.getString(startIndex + INDEX_COLUMN_MB_ID)); artist.setArtistName(cursor.getString(startIndex + INDEX_COLUMN_NAME)); artist.setHidden(SqliteUtil.loadBoolean(cursor, INDEX_COLUMN_IS_HIDDEN)); return artist; } public static String create() { return createTable(NAME, COLUMN_ID, TYPE_COLUMN_ID, COLUMN_ANDROID_ID, TYPE_COLUMN_ANDROID_ID, COLUMN_MB_ID, TYPE_COLUMN_MB_ID, COLUMN_NAME, TYPE_COLUMN_NAME, COLUMN_DATE_CREATED, TYPE_COLUMN_DATE_CREATED, COLUMN_IS_HIDDEN, TYPE_COLUMN_IS_HIDDEN); } public static ContentValues toContentValues(Artist artist) { ContentValues values = new ContentValues(); SqliteUtil.putIfNotNull(values, COLUMN_ANDROID_ID, artist.getAndroidAudioArtistId()); SqliteUtil.putIfNotNull(values, COLUMN_MB_ID, artist.getMusicBrainzId()); SqliteUtil .putIfNotNull(values, COLUMN_NAME, artist.getArtistName()); SqliteUtil.putIfNotNull(values, COLUMN_DATE_CREATED, DateUtil.toLong(artist.getDateCreated())); SqliteUtil .putIfNotNull(values, COLUMN_IS_HIDDEN, artist.isHidden()); return values; } } }