1/* 2* Copyright (c) 2011-2013 Yorba Foundation 3* 4* This program is free software; you can redistribute it and/or 5* modify it under the terms of the GNU Lesser General Public 6* License as published by the Free Software Foundation; either 7* version 2.1 of the License, or (at your option) any later version. 8* 9* This program is distributed in the hope that it will be useful, 10* but WITHOUT ANY WARRANTY; without even the implied warranty of 11* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12* General Public License for more details. 13* 14* You should have received a copy of the GNU General Public 15* License along with this program; if not, write to the 16* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 17* Boston, MA 02110-1301 USA 18*/ 19 20public struct TagID { 21 public const int64 INVALID = -1; 22 23 public int64 id; 24 25 public TagID (int64 id = INVALID) { 26 this.id = id; 27 } 28 29 public bool is_invalid () { 30 return (id == INVALID); 31 } 32 33 public bool is_valid () { 34 return (id != INVALID); 35 } 36} 37 38public class TagRow { 39 public TagID tag_id; 40 public string name; 41 public Gee.Set<string>? source_id_list; 42 public int64 time_created; 43} 44 45public class TagTable : DatabaseTable { 46 private static TagTable instance = null; 47 48 private TagTable () { 49 set_table_name ("TagTable"); 50 51 var stmt = create_stmt ("CREATE TABLE IF NOT EXISTS " 52 + "TagTable " 53 + "(" 54 + "id INTEGER PRIMARY KEY, " 55 + "name TEXT UNIQUE NOT NULL, " 56 + "photo_id_list TEXT, " 57 + "time_created INTEGER" 58 + ")"); 59 60 var res = stmt.step (); 61 if (res != Sqlite.DONE) 62 fatal ("create TagTable", res); 63 } 64 65 public static TagTable get_instance () { 66 if (instance == null) 67 instance = new TagTable (); 68 69 return instance; 70 } 71 72 public static void upgrade_for_htags () { 73 TagTable table = get_instance (); 74 75 try { 76 Gee.List < TagRow?> rows = table.get_all_rows (); 77 78 foreach (TagRow row in rows) { 79 row.name = row.name.replace (Tag.PATH_SEPARATOR_STRING, "-"); 80 table.rename (row.tag_id, row.name); 81 } 82 } catch (DatabaseError e) { 83 error ("TagTable: can't upgrade tag names for hierarchical tag support: %s", e.message); 84 } 85 } 86 87 public TagRow add (string name) throws DatabaseError { 88 var stmt = create_stmt ("INSERT INTO TagTable (name, time_created) VALUES (?, ?)"); 89 90 int64 time_created = now_sec (); 91 92 bind_text (stmt, 1, name); 93 bind_int64 (stmt, 2, time_created); 94 95 var res = stmt.step (); 96 if (res != Sqlite.DONE) 97 throw_error ("TagTable.add", res); 98 99 TagRow row = new TagRow (); 100 row.tag_id = TagID (db.last_insert_rowid ()); 101 row.name = name; 102 row.source_id_list = null; 103 row.time_created = time_created; 104 105 return row; 106 } 107 108 // All fields but tag_id are respected in TagRow. 109 public TagID create_from_row (TagRow row) throws DatabaseError { 110 var stmt = create_stmt ("INSERT INTO TagTable (name, photo_id_list, time_created) VALUES (?, ?, ?)"); 111 112 bind_text (stmt, 1, row.name); 113 bind_text (stmt, 2, serialize_source_ids (row.source_id_list)); 114 bind_int64 (stmt, 3, row.time_created); 115 116 var res = stmt.step (); 117 if (res != Sqlite.DONE) 118 throw_error ("TagTable.create_from_row", res); 119 120 return TagID (db.last_insert_rowid ()); 121 } 122 123 public void remove (TagID tag_id) throws DatabaseError { 124 delete_by_id (tag_id.id); 125 } 126 127 public string? get_name (TagID tag_id) throws DatabaseError { 128 Sqlite.Statement stmt; 129 if (!select_by_id (tag_id.id, "name", out stmt)) 130 return null; 131 132 return stmt.column_text (0); 133 } 134 135 public TagRow? get_row (TagID tag_id) throws DatabaseError { 136 var stmt = create_stmt ("SELECT name, photo_id_list, time_created FROM TagTable WHERE id=?"); 137 138 bind_int64 (stmt, 1, tag_id.id); 139 140 var res = stmt.step (); 141 if (res == Sqlite.DONE) 142 return null; 143 else if (res != Sqlite.ROW) 144 throw_error ("TagTable.get_row", res); 145 146 TagRow row = new TagRow (); 147 row.tag_id = tag_id; 148 row.name = stmt.column_text (0); 149 row.source_id_list = unserialize_source_ids (stmt.column_text (1)); 150 row.time_created = stmt.column_int64 (2); 151 152 return row; 153 } 154 155 public Gee.List < TagRow?> get_all_rows () throws DatabaseError { 156 var stmt = create_stmt ("SELECT id, name, photo_id_list, time_created FROM TagTable"); 157 158 Gee.List < TagRow?> rows = new Gee.ArrayList < TagRow?> (); 159 160 for (;;) { 161 var res = stmt.step (); 162 if (res == Sqlite.DONE) 163 break; 164 else if (res != Sqlite.ROW) 165 throw_error ("TagTable.get_all_rows", res); 166 167 // res == Sqlite.ROW 168 TagRow row = new TagRow (); 169 row.tag_id = TagID (stmt.column_int64 (0)); 170 row.name = stmt.column_text (1); 171 row.source_id_list = unserialize_source_ids (stmt.column_text (2)); 172 row.time_created = stmt.column_int64 (3); 173 174 rows.add (row); 175 } 176 177 return rows; 178 } 179 180 public void rename (TagID tag_id, string new_name) throws DatabaseError { 181 update_text_by_id_2 (tag_id.id, "name", new_name); 182 } 183 184 public void set_tagged_sources (TagID tag_id, Gee.Collection<string> source_ids) throws DatabaseError { 185 var stmt = create_stmt ("UPDATE TagTable SET photo_id_list=? WHERE id=?"); 186 187 var serialized = serialize_source_ids (source_ids); 188 if (serialized == null) { 189 bind_null (stmt, 1); 190 } else { 191 bind_text (stmt, 1, serialized); 192 } 193 194 bind_int64 (stmt, 2, tag_id.id); 195 196 var res = stmt.step (); 197 if (res != Sqlite.DONE) 198 throw_error ("TagTable.set_tagged_photos", res); 199 } 200 201 private string? serialize_source_ids (Gee.Collection<string>? source_ids) { 202 if (source_ids == null) 203 return null; 204 205 StringBuilder result = new StringBuilder (); 206 207 foreach (string source_id in source_ids) { 208 result.append (source_id); 209 result.append (","); 210 } 211 212 return (result.len != 0) ? result.str : null; 213 } 214 215 private Gee.Set<string> unserialize_source_ids (string? text_list) { 216 Gee.Set<string> result = new Gee.HashSet<string> (); 217 218 if (text_list == null) 219 return result; 220 221 string[] split = text_list.split (","); 222 foreach (string token in split) { 223 if (is_string_empty (token)) 224 continue; 225 226 // handle current and legacy encoding of source ids -- in the past, we only stored 227 // LibraryPhotos in tags so we only needed to store the numeric database key of the 228 // photo to uniquely identify it. Now, however, tags can store arbitrary MediaSources, 229 // so instead of simply storing a number we store the source id, a string that contains 230 // a typename followed by an identifying number (e.g., "video-022354"). 231 if (token[0].isdigit ()) { 232 // this is a legacy entry 233 result.add (PhotoID.upgrade_photo_id_to_source_id (PhotoID (parse_int64 (token, 10)))); 234 } else if (token[0].isalpha ()) { 235 // this is a modern entry 236 result.add (token); 237 } 238 } 239 240 return result; 241 } 242} 243