org.solovyev.android.messenger.chats.SqliteChatDao.java Source code

Java tutorial

Introduction

Here is the source code for org.solovyev.android.messenger.chats.SqliteChatDao.java

Source

/*
 * Copyright 2013 serso aka se.solovyev
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.solovyev.android.messenger.chats;

import android.app.Application;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.inject.Inject;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.solovyev.android.db.*;
import org.solovyev.android.db.properties.PropertyByIdDbQuery;
import org.solovyev.android.messenger.LinkedEntitiesDao;
import org.solovyev.android.messenger.MergeDaoResult;
import org.solovyev.android.messenger.accounts.AccountState;
import org.solovyev.android.messenger.db.StringIdMapper;
import org.solovyev.android.messenger.entities.Entity;
import org.solovyev.android.messenger.entities.EntityMapper;
import org.solovyev.android.messenger.messages.Message;
import org.solovyev.android.messenger.messages.SqliteMessageDao;
import org.solovyev.android.messenger.users.User;
import org.solovyev.android.messenger.users.UserService;
import org.solovyev.android.properties.AProperty;
import org.solovyev.common.Converter;
import org.solovyev.common.collections.Collections;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Singleton;
import java.util.*;

import static com.google.common.collect.Iterables.find;
import static com.google.common.collect.Iterables.transform;
import static org.solovyev.android.db.AndroidDbUtils.*;
import static org.solovyev.common.text.Strings.isEmpty;

@Singleton
public class SqliteChatDao extends AbstractSQLiteHelper implements ChatDao {

    /*
    **********************************************************************
    *
    *                           AUTO INJECTED FIELDS
    *
    **********************************************************************
    */

    @Inject
    @Nonnull
    private UserService userService;

    /*
    **********************************************************************
    *
    *                           FIELDS
    *
    **********************************************************************
    */

    @Nonnull
    private final Dao<Chat> dao;

    @Nonnull
    private final LinkedEntitiesDao<Chat> linkedEntitiesDao;

    @Inject
    public SqliteChatDao(@Nonnull Application context, @Nonnull SQLiteOpenHelper sqliteOpenHelper) {
        super(context, sqliteOpenHelper);
        final ChatDaoMapper chatDaoMapper = new ChatDaoMapper(this);
        dao = new SqliteDao<Chat>("chats", "id", chatDaoMapper, context, sqliteOpenHelper);
        linkedEntitiesDao = new SqliteLinkedEntitiesDao<Chat>("chats", "id", context, sqliteOpenHelper,
                "user_chats", "user_id", "chat_id", dao);
    }

    @Nonnull
    @Override
    public Collection<String> readLinkedEntityIds(@Nonnull String userId) {
        return linkedEntitiesDao.readLinkedEntityIds(userId);
    }

    @Override
    public long update(@Nonnull Chat chat) {
        final long rows = dao.update(chat);
        if (rows >= 0) {
            doDbExecs(getSqliteOpenHelper(),
                    Arrays.<DbExec>asList(new DeleteChatProperties(chat), new InsertChatProperties(chat)));
        }

        return rows;
    }

    @Override
    public void deleteAll() {
        doDbExec(getSqliteOpenHelper(), DeleteAllRowsDbExec.newInstance("user_chats"));
        doDbExec(getSqliteOpenHelper(), DeleteAllRowsDbExec.newInstance("chat_properties"));
        dao.deleteAll();
    }

    @Nonnull
    @Override
    public Map<Entity, Integer> getUnreadChats() {
        return doDbQuery(getSqliteOpenHelper(), new UnreadChatsLoader(getContext(), getSqliteOpenHelper()));
    }

    @Override
    public void delete(@Nonnull User user, @Nonnull Chat chat) {
        doDbExec(getSqliteOpenHelper(), new RemoveChats(user.getId(), chat));
    }

    @Nonnull
    @Override
    public List<String> readLastChatIds(@Nullable String userId, boolean privateChat, int count) {
        return doDbQuery(getSqliteOpenHelper(), new LoadLastChatIds(userId, privateChat, count));
    }

    @Nonnull
    @Override
    public Collection<String> readAllIds() {
        return dao.readAllIds();
    }

    @Nonnull
    @Override
    public List<AProperty> readPropertiesById(@Nonnull String chatId) {
        return doDbQuery(getSqliteOpenHelper(),
                new LoadChatPropertiesDbQuery(chatId, getContext(), getSqliteOpenHelper()));
    }

    @Nonnull
    @Override
    public List<Chat> readChatsByUserId(@Nonnull String userId) {
        return doDbQuery(getSqliteOpenHelper(),
                new LoadChatsByUserId(getContext(), userId, getSqliteOpenHelper(), this));
    }

    @Nonnull
    @Override
    public List<User> readParticipants(@Nonnull String chatId) {
        return doDbQuery(getSqliteOpenHelper(),
                new LoadChatParticipants(getContext(), chatId, userService, getSqliteOpenHelper()));
    }

    @Override
    public Chat read(@Nonnull String chatId) {
        return dao.read(chatId);
    }

    @Nonnull
    @Override
    public Collection<Chat> readAll() {
        return dao.readAll();
    }

    @Override
    public long create(@Nonnull Chat chat) {
        return dao.create(chat);
    }

    @Override
    public void delete(@Nonnull Chat chat) {
        deleteById(chat.getId());
    }

    @Override
    public void deleteById(@Nonnull String id) {
        dao.deleteById(id);
    }

    private static final class LoadChatParticipants extends AbstractDbQuery<List<User>> {

        @Nonnull
        private final String chatId;

        @Nonnull
        private final UserService userService;

        private LoadChatParticipants(@Nonnull Context context, @Nonnull String chatId,
                @Nonnull UserService userService, @Nonnull SQLiteOpenHelper sqliteOpenHelper) {
            super(context, sqliteOpenHelper);
            this.chatId = chatId;
            this.userService = userService;
        }

        @Nonnull
        @Override
        public Cursor createCursor(@Nonnull SQLiteDatabase db) {
            return db.query("user_chats", null, "chat_id = ? ", new String[] { chatId }, null, null, null);
        }

        @Nonnull
        @Override
        public List<User> retrieveData(@Nonnull Cursor cursor) {
            return new ListMapper<User>(new ChatParticipantMapper(userService)).convert(cursor);
        }
    }

    private static final class LoadChatsByUserId extends AbstractDbQuery<List<Chat>> {

        @Nonnull
        private final String userId;

        @Nonnull
        private final ChatDao chatDao;

        private LoadChatsByUserId(@Nonnull Context context, @Nonnull String userId,
                @Nonnull SQLiteOpenHelper sqliteOpenHelper, @Nonnull ChatDao chatDao) {
            super(context, sqliteOpenHelper);
            this.userId = userId;
            this.chatDao = chatDao;
        }

        @Nonnull
        @Override
        public Cursor createCursor(@Nonnull SQLiteDatabase db) {
            return db.query("chats", null, "id in (select chat_id from user_chats where user_id = ? ) ",
                    new String[] { userId }, null, null, null);
        }

        @Nonnull
        @Override
        public List<Chat> retrieveData(@Nonnull Cursor cursor) {
            return new ListMapper<Chat>(new ChatMapper(chatDao)).convert(cursor);
        }
    }

    public static final class LoadChatPropertiesDbQuery extends PropertyByIdDbQuery {

        public LoadChatPropertiesDbQuery(@Nonnull String chatId, @Nonnull Context context,
                @Nonnull SQLiteOpenHelper sqliteOpenHelper) {
            super(context, sqliteOpenHelper, "chat_properties", "chat_id", chatId);
        }
    }

    @Nonnull
    @Override
    public ChatMergeDaoResult mergeChats(@Nonnull String userId, @Nonnull Iterable<? extends AccountChat> chats) {
        final ChatMergeDaoResult result = new ChatMergeDaoResult(mergeLinkedEntities(userId, chats));

        final List<DbExec> execs = new ArrayList<DbExec>();

        for (final Chat addedChat : result.getAddedObjects()) {
            final AccountChat chat = find(chats, new Predicate<AccountChat>() {
                @Override
                public boolean apply(AccountChat chat) {
                    return chat.getChat().equals(addedChat);
                }
            });

            for (Message message : chat.getMessages()) {
                execs.add(new SqliteMessageDao.InsertMessage(message));
            }
            result.addNewMessages(addedChat, chat.getMessages());

            for (User participant : chat.getParticipants()) {
                final String participantId = participant.getId();
                if (!participantId.equals(userId)) {
                    execs.add(new InsertChatLink(participantId, addedChat.getId()));
                }
            }
        }

        doDbExecs(getSqliteOpenHelper(), execs);

        return result;
    }

    private MergeDaoResult<Chat, String> mergeLinkedEntities(@Nonnull String userId,
            Iterable<? extends AccountChat> apiChats) {
        // !!! actually not all chats are loaded and we cannot delete the chat just because it is not in the list
        return mergeLinkedEntities(userId, transform(apiChats, new Function<AccountChat, Chat>() {
            @Override
            public Chat apply(@Nullable AccountChat accountChat) {
                assert accountChat != null;
                return accountChat.getChat();
            }
        }), false, true);
    }

    @Nonnull
    @Override
    public MergeDaoResult<Chat, String> mergeLinkedEntities(@Nonnull String userId,
            @Nonnull Iterable<Chat> linkedEntities, boolean allowRemoval, boolean allowUpdate) {
        final MergeDaoResult<Chat, String> result = linkedEntitiesDao.mergeLinkedEntities(userId, linkedEntities,
                allowRemoval, allowUpdate);

        final List<DbExec> execs = new ArrayList<DbExec>();

        if (!result.getRemovedObjectIds().isEmpty()) {
            execs.addAll(RemoveChats.newInstances(userId, result.getRemovedObjectIds()));
        }

        for (Chat updatedChat : result.getUpdatedObjects()) {
            execs.add(new UpdateChat(updatedChat));
            execs.add(new DeleteChatProperties(updatedChat));
            execs.add(new InsertChatProperties(updatedChat));
        }

        for (final Chat addedChat : result.getAddedObjects()) {
            execs.add(new InsertChat(addedChat));
            execs.add(new InsertChatProperties(addedChat));
            execs.add(new InsertChatLink(userId, addedChat.getEntity().getEntityId()));
        }

        doDbExecs(getSqliteOpenHelper(), execs);

        return result;
    }

    private static final class RemoveChats implements DbExec {

        @Nonnull
        private String userId;

        @Nonnull
        private List<String> chatIds;

        private RemoveChats(@Nonnull String userId, @Nonnull List<String> chatIds) {
            this.userId = userId;
            this.chatIds = chatIds;
        }

        private RemoveChats(@Nonnull String userId, @Nonnull Chat chat) {
            this.userId = userId;
            this.chatIds = Arrays.asList(chat.getId());
        }

        @Nonnull
        private static List<RemoveChats> newInstances(@Nonnull String userId, @Nonnull List<String> chatIds) {
            final List<RemoveChats> result = new ArrayList<RemoveChats>();

            for (List<String> chatIdsChunk : Collections.split(chatIds, MAX_IN_COUNT)) {
                result.add(new RemoveChats(userId, chatIdsChunk));
            }

            return result;
        }

        @Override
        public long exec(@Nonnull SQLiteDatabase db) {
            return db.delete("user_chats", "user_id = ? and chat_id in " + AndroidDbUtils.inClause(chatIds),
                    AndroidDbUtils.inClauseValues(chatIds, userId));
        }
    }

    private static final class UpdateChat extends AbstractObjectDbExec<Chat> {

        private UpdateChat(@Nonnull Chat chat) {
            super(chat);
        }

        @Override
        public long exec(@Nonnull SQLiteDatabase db) {
            final Chat chat = getNotNullObject();

            final ContentValues values = toContentValues(chat);

            return db.update("chats", values, "id = ?",
                    new String[] { String.valueOf(chat.getEntity().getEntityId()) });
        }
    }

    private static final class InsertChat extends AbstractObjectDbExec<Chat> {

        private InsertChat(@Nonnull Chat chat) {
            super(chat);
        }

        @Override
        public long exec(@Nonnull SQLiteDatabase db) {
            final Chat chat = getNotNullObject();

            final ContentValues values = toContentValues(chat);

            return db.insert("chats", null, values);
        }
    }

    @Nonnull
    private static ContentValues toContentValues(@Nonnull Chat chat) {
        final DateTimeFormatter dateTimeFormatter = ISODateTimeFormat.basicDateTime();

        final DateTime lastMessagesSyncDate = chat.getLastMessagesSyncDate();

        final ContentValues values = new ContentValues();

        values.put("id", chat.getEntity().getEntityId());
        values.put("account_id", chat.getEntity().getAccountId());
        values.put("account_chat_id", chat.getEntity().getAccountEntityId());
        values.put("last_messages_sync_date",
                lastMessagesSyncDate == null ? null : dateTimeFormatter.print(lastMessagesSyncDate));

        return values;
    }

    private static final class DeleteChatProperties extends AbstractObjectDbExec<Chat> {

        private DeleteChatProperties(@Nonnull Chat chat) {
            super(chat);
        }

        @Override
        public long exec(@Nonnull SQLiteDatabase db) {
            final Chat chat = getNotNullObject();

            return db.delete("chat_properties", "chat_id = ?",
                    new String[] { String.valueOf(chat.getEntity().getEntityId()) });
        }
    }

    private static final class InsertChatProperties extends AbstractObjectDbExec<Chat> {

        private InsertChatProperties(@Nonnull Chat chat) {
            super(chat);
        }

        @Override
        public long exec(@Nonnull SQLiteDatabase db) {
            long result = 0;
            final Chat chat = getNotNullObject();

            for (AProperty property : chat.getPropertiesCollection()) {
                final ContentValues values = new ContentValues();
                values.put("chat_id", chat.getEntity().getEntityId());
                values.put("property_name", property.getName());
                values.put("property_value", property.getValue());
                final long id = db.insert("chat_properties", null, values);
                if (id == SQL_ERROR) {
                    result = SQL_ERROR;
                }
            }

            return result;
        }
    }

    private static final class InsertChatLink implements DbExec {

        @Nonnull
        private String userId;

        @Nonnull
        private String chatId;

        private InsertChatLink(@Nonnull String userId, @Nonnull String chatId) {
            this.userId = userId;
            this.chatId = chatId;
        }

        @Override
        public long exec(@Nonnull SQLiteDatabase db) {
            final ContentValues values = new ContentValues();
            values.put("user_id", userId);
            values.put("chat_id", chatId);
            return db.insert("user_chats", null, values);
        }
    }

    private static final class UnreadChatsLoader extends AbstractDbQuery<Map<Entity, Integer>> {

        protected UnreadChatsLoader(@Nonnull Context context, @Nonnull SQLiteOpenHelper sqliteOpenHelper) {
            super(context, sqliteOpenHelper);
        }

        @Nonnull
        @Override
        public Cursor createCursor(@Nonnull SQLiteDatabase db) {
            return db.rawQuery("select c.id, c.account_id, c.account_chat_id, count(*) from chats c, messages m "
                    + "where c.id = m.chat_id " + "and m.read = 0 " + "and m.state = 'received' "
                    + "group by c.id, c.account_id, c.account_chat_id", null);
        }

        @Nonnull
        @Override
        public Map<Entity, Integer> retrieveData(@Nonnull Cursor cursor) {
            final Map<Entity, Integer> result = new HashMap<Entity, Integer>();

            if (cursor.moveToFirst()) {
                while (!cursor.isAfterLast()) {
                    final int unreadMessagesCount = cursor.getInt(3);
                    if (unreadMessagesCount > 0) {
                        result.put(EntityMapper.newInstanceFor(0).convert(cursor), unreadMessagesCount);
                    }
                    cursor.moveToNext();
                }
            }

            return result;
        }
    }

    private static final class ChatDaoMapper implements SqliteDaoEntityMapper<Chat> {

        @Nonnull
        private final ChatMapper chatMapper;

        private ChatDaoMapper(@Nonnull ChatDao dao) {
            chatMapper = new ChatMapper(dao);
        }

        @Nonnull
        @Override
        public ContentValues toContentValues(@Nonnull Chat chat) {
            return SqliteChatDao.toContentValues(chat);
        }

        @Nonnull
        @Override
        public Converter<Cursor, Chat> getCursorMapper() {
            return chatMapper;
        }
    }

    private class LoadLastChatIds extends AbstractDbQuery<List<String>> {

        @Nullable
        private final String userId;

        private final boolean privateChat;

        private final int count;

        public LoadLastChatIds(@Nullable String userId, boolean privateChat, int count) {
            super(SqliteChatDao.this.getContext(), SqliteChatDao.this.getSqliteOpenHelper());
            this.userId = userId;
            this.privateChat = privateChat;
            this.count = count;
        }

        @Nonnull
        @Override
        public Cursor createCursor(@Nonnull SQLiteDatabase db) {
            String start = "select c.id, m.send_time from chats c, messages m, user_chats uc where c.id = m.chat_id and uc.chat_id = c.id ";
            if (privateChat) {
                start += "and exists (select * from chat_properties cp where cp.chat_id = c.id and cp.property_name = 'private' and cp.property_value = 'true') ";
            }
            final String end = "group by c.id order by m.send_time desc limit " + count;
            if (!isEmpty(userId)) {
                return db.rawQuery(start + "and uc.user_id = ? " + end, new String[] { userId });
            } else {
                return db.rawQuery(
                        start + "and uc.user_id in (select a.user_id from accounts a where a.state = ?) " + end,
                        new String[] { AccountState.enabled.name() });
            }
        }

        @Nonnull
        @Override
        public List<String> retrieveData(@Nonnull Cursor cursor) {
            return new ListMapper<String>(StringIdMapper.getInstance()).convert(cursor);
        }
    }
}