EntityDiff.java revision 2ae666ec99ae9318936a9326e5243987e4e1c586
1/*
2 * Copyright (C) 2009 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 com.android.contacts.model;
18
19import android.content.ContentProviderOperation;
20import android.content.ContentValues;
21import android.content.Entity;
22import android.content.ContentProviderOperation.Builder;
23import android.content.Entity.NamedContentValues;
24import android.net.Uri;
25import android.provider.BaseColumns;
26
27import java.util.ArrayList;
28import java.util.HashMap;
29
30
31/**
32 * Describes a set of {@link ContentProviderOperation} that need to be
33 * executed to transform a database from one {@link Entity} to another.
34 */
35public class EntityDiff extends ArrayList<ContentProviderOperation> {
36    private EntityDiff() {
37    }
38
39    /**
40     * Build the set of {@link ContentProviderOperation} needed to translate
41     * from "before" to "after". Tries its best to keep operations to
42     * minimal number required. Assumes that all {@link ContentValues} are
43     * keyed using {@link BaseColumns#_ID} values.
44     */
45    public static EntityDiff buildDiff(Entity before, Entity after, Uri targetUri,
46            String childForeignKey) {
47        final EntityDiff diff = new EntityDiff();
48
49        Builder builder;
50        ContentValues values;
51
52        if (before == null) {
53            // Before doesn't exist, so insert "after" values
54            builder = ContentProviderOperation.newInsert(targetUri);
55            builder.withValues(after.getEntityValues());
56            diff.add(builder.build());
57
58            for (NamedContentValues child : after.getSubValues()) {
59                // Add builder with reference to original _id when needed
60                builder = ContentProviderOperation.newInsert(child.uri);
61                builder.withValues(child.values);
62                if (childForeignKey != null) {
63                    builder.withValueBackReference(childForeignKey, 0);
64                }
65                diff.add(builder.build());
66            }
67
68        } else if (after == null) {
69            // After doesn't exist, so delete "before" values
70            for (NamedContentValues child : before.getSubValues()) {
71                builder = ContentProviderOperation.newDelete(child.uri);
72                builder.withSelection(getSelectIdClause(child.values), null);
73                diff.add(builder.build());
74            }
75
76            builder = ContentProviderOperation.newDelete(targetUri);
77            builder.withSelection(getSelectIdClause(before.getEntityValues()), null);
78            diff.add(builder.build());
79
80        } else {
81            // Somewhere between, so update any changed values
82            values = after.getEntityValues();
83            if (!before.getEntityValues().equals(values)) {
84                // Top-level values changed, so update
85                builder = ContentProviderOperation.newUpdate(targetUri);
86                builder.withSelection(getSelectIdClause(values), null);
87                builder.withValues(values);
88                diff.add(builder.build());
89            }
90
91            // Build lookup maps for children on both sides
92            final HashMap<String, NamedContentValues> beforeChildren = buildChildrenMap(before);
93            final HashMap<String, NamedContentValues> afterChildren = buildChildrenMap(after);
94
95            // Walk through "before" children looking for deletes and updates
96            for (NamedContentValues beforeChild : beforeChildren.values()) {
97                final String key = buildChildKey(beforeChild);
98                final NamedContentValues afterChild = afterChildren.get(key);
99
100                if (afterChild == null) {
101                    // After child doesn't exist, so delete "before" child
102                    builder = ContentProviderOperation.newDelete(beforeChild.uri);
103                    builder.withSelection(getSelectIdClause(beforeChild.values), null);
104                    diff.add(builder.build());
105                } else if (!beforeChild.values.equals(afterChild.values)) {
106                    // After child still exists, and is different, so update
107                    values = afterChild.values;
108                    builder = ContentProviderOperation.newUpdate(afterChild.uri);
109                    builder.withSelection(getSelectIdClause(values), null);
110                    builder.withValues(values);
111                    diff.add(builder.build());
112                }
113
114                // Remove the now-handled "after" child
115                afterChildren.remove(key);
116            }
117
118            // Walk through remaining "after" children, which are inserts
119            for (NamedContentValues afterChild : afterChildren.values()) {
120                builder = ContentProviderOperation.newInsert(afterChild.uri);
121                builder.withValues(afterChild.values);
122                diff.add(builder.build());
123            }
124        }
125
126        return diff;
127    }
128
129    private static String buildChildKey(NamedContentValues child) {
130        return child.uri.toString() + child.values.getAsString(BaseColumns._ID);
131    }
132
133    private static String getSelectIdClause(ContentValues values) {
134        return BaseColumns._ID + "=" + values.getAsLong(BaseColumns._ID);
135    }
136
137    private static HashMap<String, NamedContentValues> buildChildrenMap(Entity entity) {
138        final ArrayList<NamedContentValues> children = entity.getSubValues();
139        final HashMap<String, NamedContentValues> childrenMap = new HashMap<String, NamedContentValues>(
140                children.size());
141        for (NamedContentValues child : children) {
142            final String key = buildChildKey(child);
143            childrenMap.put(key, child);
144        }
145        return childrenMap;
146    }
147}
148