/* * imagedb.c - implementation of image database */ #include #include #include #include #include "lodju.h" /*********************************************************************** * Attribute information for Images and ImageFolders. */ static GHashTable *create_attributes(void) { return g_hash_table_new(g_str_hash, g_str_equal); } static const gchar *get_attribute(GHashTable *hash, const gchar *name) { gpointer value; value = g_hash_table_lookup(hash, name); if (value != NULL) return value; return ""; } static void remove_key_value(GHashTable *hash, const gchar *key) { gboolean found; gpointer orig_key; gpointer orig_value; found = g_hash_table_lookup_extended(hash, key, &orig_key, &orig_value); if (found) { g_hash_table_remove(hash, key); g_free(orig_key); g_free(orig_value); } } static void set_attribute(GHashTable *hash, const gchar *name, const gchar *value) { gchar *name2; gchar *value2; remove_key_value(hash, name); name2 = g_strdup(name); value2 = g_strdup(value); g_hash_table_insert(hash, name2, value2); } static void remember_key(gpointer key, gpointer value, gpointer user_data) { GList **listptr; listptr = user_data; *listptr = g_list_prepend(*listptr, key); } static void destroy_attributes(GHashTable *hash) { GList *keys; keys = NULL; g_hash_table_foreach(hash, remember_key, &keys); while (keys != NULL) { remove_key_value(hash, keys->data); keys = g_list_remove_link(keys, keys); } g_hash_table_destroy(hash); } /*********************************************************************** * Foo. */ static gchar *encode_ampersand(const gchar *str) { gchar *encoded; gint i; gint j; gint count; count = 0; for (i = 0; str[i] != '\0'; ++i) if (str[i] == '&') ++count; encoded = g_new(gchar, i + 1 + count * 4); j = 0; for (i = 0; str[i] != '\0'; ++i) { if (str[i] == '&') { encoded[j++] = '&'; encoded[j++] = 'a'; encoded[j++] = 'm'; encoded[j++] = 'p'; encoded[j++] = ';'; } else { encoded[j++] = str[i]; } } encoded[j] = '\0'; return encoded; } /*********************************************************************** * Define the Image data type and operations on it. */ struct Image { GHashTable *hash; gboolean dirty; }; static Image *image_create(const gchar *filename) { Image *image; image = g_new(Image, 1); image->hash = create_attributes(); image->dirty = TRUE; image_set_attribute(image, "filename", filename); image_set_attribute(image, "rotation", "0"); image_set_attribute(image, "privacy", "private"); return image; } static Image *image_parse(xmlNodePtr node) { xmlNodePtr child; Image *image; gchar *name; gchar *caption; gchar *rotation; gchar *privacy; image = NULL; name = NULL; caption = NULL; rotation = NULL; privacy = NULL; for (child = node->childs; child != NULL; child = child->next) { if (child->name == NULL) continue; if (strcmp(child->name, "filespec") == 0) name = node_get_attribute(child, "uri"); else if (strcmp(child->name, "title") == 0) caption = node_get_content(child); else if (strcmp(child->name, "rotation") == 0) rotation = node_get_content(child); else if (strcmp(child->name, "privacy") == 0) privacy = node_get_attribute(child, "level"); } if (name != NULL) { image = image_create(name); if (caption != NULL) image_set_attribute(image, "caption", caption); if (rotation != NULL) image_set_attribute(image, "rotation", rotation); if (privacy != NULL) image_set_attribute(image, "privacy", privacy); } g_free(name); g_free(caption); g_free(rotation); g_free(privacy); return image; } static void image_destroy(gpointer data, gpointer user_data) { Image *image; if (data != NULL) { image = data; destroy_attributes(image->hash); g_free(image); } } static gboolean image_is_dirty(Image *image) { return image->dirty; } static void image_mark_unmodified(Image *image) { image->dirty = FALSE; } const gchar *image_get_attribute(Image *image, const gchar *name) { return get_attribute(image->hash, name); } void image_set_attribute(Image *image, const gchar *name, const gchar *value) { const gchar *old_value; old_value = get_attribute(image->hash, name); if (value != NULL && strcmp(old_value, value) == 0) return; set_attribute(image->hash, name, value); image->dirty = TRUE; } static xmlNodePtr xml_new_child(xmlNodePtr parent, const gchar *name, const gchar *value) { gchar *str; xmlNodePtr child; if (value == NULL) str = NULL; else str = encode_ampersand(value); child = xmlNewChild(parent, NULL, name, str); g_free(str); return child; } static void image_to_xml(gpointer data, gpointer user_data) { Image *image; xmlNodePtr parent; xmlNodePtr node; xmlNodePtr filespec; xmlNodePtr privacy; const gchar *value; gchar *str; image = data; parent = user_data; node = xmlNewChild(parent, NULL, "image", NULL); filespec = xmlNewChild(node, NULL, "filespec", NULL); str = encode_ampersand(image_get_attribute(image, "filename")); xmlSetProp(filespec, "uri", str); g_free(str); xml_new_child(node, "title", image_get_attribute(image, "caption")); xml_new_child(node, "rotation", image_get_attribute(image, "rotation")); privacy = xml_new_child(node, "privacy", NULL); value = image_get_attribute(image, "privacy"); if (value == NULL || *value == '\0') value = "private"; xmlSetProp(privacy, "level", value); } gchar *image_get_xml(Image *image) { xmlDocPtr doc; xmlChar *str; int size; doc = xmlNewDoc("1.0"); doc->root = xmlNewDocNode(doc, NULL, "lodju-dnd-image", NULL); image_to_xml(image, doc->root); xmlDocDumpMemory(doc, &str, &size); xmlFreeDoc(doc); return (gchar *) str; } /*********************************************************************** * Define the ImageFolder data type and operations on it. */ struct ImageFolder { ImageFolder *parent; GHashTable *hash; GList *images; GList *folders; gboolean dirty; }; static ImageFolder *image_folder_init(const gchar *name) { ImageFolder *folder; folder = g_new(ImageFolder, 1); folder->parent = NULL; folder->hash = create_attributes(); folder->images = NULL; folder->folders = NULL; folder->dirty = TRUE; image_folder_set_attribute(folder, "name", name); return folder; } static ImageFolder *image_folder_create_root(void) { return image_folder_init(_("Images")); } ImageFolder *image_folder_create(ImageFolder *parent) { ImageFolder *folder; folder = image_folder_init(_("Unnamed")); folder->parent = parent; parent->folders = g_list_append(parent->folders, folder); return folder; } static ImageFolder *folder_parse(xmlNodePtr node) { ImageFolder *folder; ImageFolder *subfolder; Image *image; xmlNodePtr child; gchar *name; if (node == NULL || node->name == NULL) return NULL; if (strcmp(node->name, "folder") != 0) return NULL; folder = image_folder_init(""); for (child = node->childs; child != NULL; child = child->next) { if (child->name == NULL) continue; if (strcmp(child->name, "image") == 0) { image = image_parse(child); if (image != NULL) folder->images = g_list_append(folder->images, image); } else if (strcmp(child->name, "folder") == 0) { subfolder = folder_parse(child); if (subfolder != NULL) { subfolder->parent = folder; folder->folders = g_list_append(folder->folders, subfolder); } } else if (strcmp(child->name, "name") == 0) { name = node_get_content(child); image_folder_set_attribute(folder, "name", name); g_free(name); } } return folder; } ImageFolder *image_folder_create_from_xml(ImageFolder *parent, const gchar *xml) { xmlDocPtr doc; ImageFolder *folder; doc = xmlParseMemory((gchar *) xml, strlen(xml)); if (doc == NULL || doc->root == NULL) goto error; if (strcmp(doc->root->name, "lodju-dnd-folder") != 0) goto error; if (doc->root->childs == NULL) goto error; if (strcmp(doc->root->childs->name, "folder") != 0) goto error; folder = folder_parse(doc->root->childs); if (folder == NULL) goto error; xmlFreeDoc(doc); folder->parent = parent; parent->folders = g_list_append(parent->folders, folder); return folder; error: if (doc != NULL) xmlFreeDoc(doc); return NULL; } static void folder_destroy_callback(gpointer data, gpointer user_data) { ImageFolder *folder; folder = data; if (folder != NULL) { destroy_attributes(folder->hash); g_list_foreach(folder->images, image_destroy, NULL); g_list_free(folder->images); g_list_foreach(folder->folders, folder_destroy_callback, NULL); g_list_free(folder->folders); g_free(folder); } } void image_folder_destroy(ImageFolder *folder) { if (folder->parent != NULL) { folder->parent->folders = g_list_remove(folder->parent->folders, folder); folder->parent->dirty = TRUE; } folder_destroy_callback(folder, NULL); } static gboolean image_folder_is_dirty(ImageFolder *folder) { GList *node; if (folder->dirty) return TRUE; for (node = folder->images; node != NULL; node = node->next) { if (image_is_dirty(node->data)) return TRUE; } for (node = folder->folders; node != NULL; node = node->next) { if (image_folder_is_dirty(node->data)) return TRUE; } return FALSE; } static void image_folder_mark_unmodified(ImageFolder *folder) { GList *node; folder->dirty = FALSE; for (node = folder->images; node != NULL; node = node->next) image_mark_unmodified(node->data); for (node = folder->folders; node != NULL; node = node->next) image_folder_mark_unmodified(node->data); } ImageFolder *image_folder_get_parent(ImageFolder *folder) { return folder->parent; } GList *image_folder_get_folders(ImageFolder *folder) { return g_list_copy(folder->folders); } Image *image_folder_import(ImageFolder *folder, const gchar *filename) { Image *image; image = image_create(filename); folder->images = g_list_append(folder->images, image); return image; } Image *image_folder_import_from_xml(ImageFolder *folder, const gchar *xml) { xmlDocPtr doc; Image *image; doc = xmlParseMemory((gchar *) xml, strlen(xml)); if (doc == NULL || doc->root == NULL) goto error; if (strcmp(doc->root->name, "lodju-dnd-image") != 0) goto error; if (doc->root->childs == NULL) goto error; if (strcmp(doc->root->childs->name, "image") != 0) goto error; image = image_parse(doc->root->childs); if (image == NULL) goto error; folder->images = g_list_append(folder->images, image); return image; error: if (doc != NULL) xmlFreeDoc(doc); return NULL; } GList *image_folder_get_images(ImageFolder *folder) { return g_list_copy(folder->images); } const gchar *image_folder_get_attribute(ImageFolder *folder, const gchar *name) { return get_attribute(folder->hash, name); } void image_folder_set_attribute(ImageFolder *folder, const gchar *name, const gchar *value) { set_attribute(folder->hash, name, value); folder->dirty = TRUE; } void image_folder_remove_image(ImageFolder *folder, Image *image) { g_assert(g_list_find(folder->images, image) != NULL); folder->images = g_list_remove(folder->images, image); folder->dirty = TRUE; image_destroy(image, NULL); } static void folder_to_xml(gpointer data, gpointer user_data) { ImageFolder *folder; xmlNodePtr parent_node; xmlNodePtr this_node; GList *list; folder = data; parent_node = user_data; this_node = xml_new_child(parent_node, "folder", NULL); xml_new_child(this_node, "name", image_folder_get_attribute(folder, "name")); list = image_folder_get_images(folder); g_list_foreach(list, image_to_xml, this_node); g_list_free(list); list = image_folder_get_folders(folder); g_list_foreach(list, folder_to_xml, this_node); g_list_free(list); } gchar *image_folder_get_xml(ImageFolder *folder) { xmlDocPtr doc; xmlChar *str; int size; doc = xmlNewDoc("1.0"); doc->root = xmlNewDocNode(doc, NULL, "lodju-dnd-folder", NULL); folder_to_xml(folder, doc->root); xmlDocDumpMemory(doc, &str, &size); xmlFreeDoc(doc); return (gchar *) str; } /*********************************************************************** * Define the ImageDB data type and operations on it. */ struct ImageDB { gchar *filename; ImageFolder *root; gboolean dirty; }; const gchar *image_db_get_name(ImageDB *imagedb) { return imagedb->filename; } void image_db_set_name(ImageDB *imagedb, const gchar *name) { g_free(imagedb->filename); if (name == NULL) imagedb->filename = NULL; else imagedb->filename = g_strdup(name); imagedb->dirty = TRUE; } static void load_database(ImageDB *imagedb) { xmlDocPtr doc; xmlNodePtr child; doc = xmlParseFile(imagedb->filename); if (doc == NULL || doc->root == NULL || doc->root->name == NULL) goto error; if (strcmp(doc->root->name, "lodju-archive") != 0) goto error; imagedb->root = NULL; for (child = doc->root->childs; child != NULL; child = child->next) { if (child->name == NULL || strcmp(child->name, "folder") != 0) continue; if (imagedb->root != NULL) continue; imagedb->root = folder_parse(child); } error: if (doc != NULL) xmlFreeDoc(doc); } static void image_db_mark_unmodified(ImageDB *imagedb) { image_folder_mark_unmodified(imagedb->root); imagedb->dirty = FALSE; } ImageDB *image_db_create(const gchar *filename) { ImageDB *imagedb; imagedb = g_new(ImageDB, 1); if (filename == NULL) imagedb->filename = NULL; else imagedb->filename = g_strdup(filename); imagedb->root = NULL; load_database(imagedb); if (imagedb->root == NULL) imagedb->root = image_folder_create_root(); if (filename == NULL) imagedb->dirty = TRUE; else image_db_mark_unmodified(imagedb); return imagedb; } void image_db_destroy(ImageDB *imagedb) { if (imagedb != NULL) { g_free(imagedb->filename); folder_destroy_callback(imagedb->root, NULL); g_free(imagedb); } } ImageFolder *image_db_get_root(ImageDB *imagedb) { return imagedb->root; } gboolean image_db_is_modified(ImageDB *imagedb) { return imagedb->dirty || image_folder_is_dirty(imagedb->root); } gint image_db_save(ImageDB *imagedb) { xmlDocPtr doc; gint ret; doc = xmlNewDoc("1.0"); doc->root = xmlNewDocNode(doc, NULL, "lodju-archive", NULL); folder_to_xml(image_db_get_root(imagedb), doc->root); if (xmlSaveFile(imagedb->filename, doc) == -1) ret = -1; else { ret = 0; image_db_mark_unmodified(imagedb); } xmlFreeDoc(doc); return ret; } /*********************************************************************** * Unit tests. */ /* * The name of the test database used with these unit tests. */ #define TEST_DB_NAME "unit_test.lodju" /* * The name of a test image used with these unit tests. */ #define TEST_PHOTO_NAME "unit_test.jpg" /* * Attributes for test images. */ static const struct { const gchar *name; const gchar *value; } attrs[] = { #if 1 /* XXX */ { "caption", "hello, world & have a nice day" }, #else { "caption", "hello, world" }, #endif { "rotation", "90" }, { "privacy", "public" }, }; static gint num_attrs = sizeof(attrs) / sizeof(attrs[0]); /* * Set attributes for a test image. */ static void set_attrs(Image *image) { gint i; for (i = 0; i < num_attrs; ++i) image_set_attribute(image, attrs[i].name, attrs[i].value); } /* * Check that the attributs of an image are those that were set. */ static gboolean check_attrs(Image *image) { gint i; const gchar *value; for (i = 0; i < num_attrs; ++i) { value = image_get_attribute(image, attrs[i].name); if (value == NULL) { g_message(_("Image attribute '%s' has no value, though it should."), attrs[i].name); return FALSE; } if (strcmp(value, attrs[i].value) != 0) { g_message(_("Image attribute '%s' has value '%s', should be '%s'."), attrs[i].name, value, attrs[i].value); return FALSE; } } return TRUE; } static ImageDB *db; static ImageFolder *root_folder; /* * This is a system specific way of removing a directory with all * its contents. It will have to be implemented in a cleaner way * at a later date. */ static void remove_database_on_disk(void) { gchar *buf; buf = g_strdup_printf("rm -rf %s", TEST_DB_NAME); (void) system(buf); g_free(buf); } static void load_db(void) { db = image_db_create(TEST_DB_NAME); root_folder = image_db_get_root(db); } static void reload_db(void) { image_db_destroy(db); load_db(); } static void setup(void) { remove_database_on_disk(); load_db(); } static void teardown(void) { image_db_destroy(db); db = NULL; root_folder = NULL; remove_database_on_disk(); } gint image_unit_test(void) { TEST("image attributes", { Image *image; image = image_create("/foo"); ASSERT(image != NULL); ASSERT(strcmp(image_get_attribute(image, "filename"), "/foo")==0); ASSERT(strcmp(image_get_attribute(image, "foo"), "") == 0); image_set_attribute(image, "foo", "bar"); ASSERT(strcmp(image_get_attribute(image, "foo"), "bar") == 0); image_destroy(image, NULL); }); TEST("folder attributes", { ImageFolder *folder; folder = image_folder_create_root(); ASSERT(folder != NULL); ASSERT(strcmp(image_folder_get_attribute(folder, "foo"), "") == 0); image_folder_set_attribute(folder, "foo", "bar"); ASSERT(strcmp(image_folder_get_attribute(folder, "foo"), "bar") == 0); image_folder_destroy(folder); }); TEST("import", { Image *image; GList *list; const gchar *filename; const gchar *caption; image = image_folder_import(root_folder, TEST_PHOTO_NAME); ASSERT(image != NULL); filename = image_get_attribute(image, "filename"); ASSERT(filename != NULL); ASSERT(strcmp(TEST_PHOTO_NAME, filename) == 0); caption = image_get_attribute(image, "caption"); ASSERT(caption != NULL); ASSERT(strcmp(caption, "") == 0); set_attrs(image); ASSERT(check_attrs(image)); root_folder = image_db_get_root(db); ASSERT(root_folder != NULL); list = image_folder_get_images(root_folder); ASSERT(list != NULL); ASSERT(g_list_length(list) == 1); ASSERT(list->data == image); g_list_free(list); }); TEST("save", { GList *list; Image *image; const gchar *filename; list = image_folder_get_images(root_folder); ASSERT(g_list_length(list) == 0); g_list_free(list); image = image_folder_import(root_folder, TEST_PHOTO_NAME); set_attrs(image); ASSERT(image_db_save(db) == 0); reload_db(); list = image_folder_get_images(root_folder); ASSERT(g_list_length(list) == 1); filename = image_get_attribute(list->data, "filename"); ASSERT(filename != NULL); ASSERT(strcmp(filename, TEST_PHOTO_NAME) == 0); ASSERT(check_attrs(list->data)); g_list_free(list); }); TEST("create folder", { ImageFolder *folder; GList *folders; ASSERT(image_folder_get_parent(root_folder) == NULL); folders = image_folder_get_folders(root_folder); ASSERT(g_list_length(folders) == 0); folder = image_folder_create(root_folder); ASSERT(folder != NULL); ASSERT(image_folder_get_parent(folder) == root_folder); image_folder_set_attribute(folder, "name", "foo & grill"); folders = image_folder_get_folders(root_folder); ASSERT(g_list_length(folders) == 1); ASSERT(folders->data == folder); ASSERT(image_db_save(db) == 0); reload_db(); ASSERT(image_folder_get_parent(root_folder) == NULL); folders = image_folder_get_folders(root_folder); ASSERT(g_list_length(folders) == 1); folder = folders->data; ASSERT(image_folder_get_parent(folder) == root_folder); ASSERT(strcmp(image_folder_get_attribute(folder, "name"), "foo & grill") == 0); }); TEST("dirty bits", { ImageFolder *folder; Image *image; ASSERT(!image_db_is_modified(db)); (void) image_folder_import(root_folder, "foo"); ASSERT(image_db_is_modified(db)); image_db_save(db); ASSERT(!image_db_is_modified(db)); folder = image_folder_create(root_folder); ASSERT(image_db_is_modified(db)); image_db_save(db); ASSERT(!image_db_is_modified(db)); image = image_folder_import(folder, "bar"); ASSERT(image_db_is_modified(db)); image_db_save(db); ASSERT(!image_db_is_modified(db)); image_folder_remove_image(folder, image); ASSERT(image_db_is_modified(db)); image_db_save(db); ASSERT(!image_db_is_modified(db)); image_folder_destroy(folder); ASSERT(image_db_is_modified(db)); image_db_save(db); ASSERT(!image_db_is_modified(db)); reload_db(); ASSERT(!image_db_is_modified(db)); }); TEST("drag and drop information for Image", { Image *image; gchar *xml; image = image_create("foo"); set_attrs(image); xml = image_get_xml(image); image_destroy(image, NULL); image = image_folder_import_from_xml(image_db_get_root(db), xml); ASSERT(check_attrs(image)); g_free(xml); }); TEST("drag and drop information for ImageFolder", { Image *image; gchar *xml; ImageFolder *folder; GList *list; image = image_folder_import(root_folder, "foo"); set_attrs(image); xml = image_folder_get_xml(root_folder); reload_db(); folder = image_folder_create_from_xml(root_folder, xml); ASSERT(folder != NULL); ASSERT(image_folder_get_parent(folder) == root_folder); list = image_folder_get_folders(root_folder); ASSERT(g_list_length(list) == 1); ASSERT(list->data == folder); g_list_free(list); list = image_folder_get_images(folder); ASSERT(g_list_length(list) == 1); ASSERT(check_attrs(list->data)); g_list_free(list); g_free(xml); }); TEST("remove image", { Image *image; GList *list; image = image_folder_import(root_folder, "foo"); list = image_folder_get_images(root_folder); ASSERT(g_list_length(list) == 1); ASSERT(list->data == image); g_list_free(list); image_folder_remove_image(root_folder, image); ASSERT(g_list_length(image_folder_get_images(root_folder)) == 0); }); TEST("remove second image", { Image *image1; Image *image2; GList *list; image1 = image_folder_import(root_folder, "foo1"); image2 = image_folder_import(root_folder, "foo2"); list = image_folder_get_images(root_folder); ASSERT(g_list_length(list) == 2); ASSERT(list->data == image1); ASSERT(list->next->data == image2); g_list_free(list); image_folder_remove_image(root_folder, image2); list = image_folder_get_images(root_folder); ASSERT(g_list_length(list) == 1); ASSERT(list->data == image1); g_list_free(list); }); TEST("folder destroy", { ImageFolder *folder; GList *list; folder = image_folder_create(root_folder); list = image_folder_get_folders(root_folder); ASSERT(g_list_length(list) == 1); ASSERT(list->data == folder); g_list_free(list); ASSERT(image_folder_get_parent(folder) == root_folder); image_folder_destroy(folder); ASSERT(g_list_length(image_folder_get_folders(root_folder)) == 0); }); TEST("database naming", { ASSERT(strcmp(image_db_get_name(db), TEST_DB_NAME) == 0); image_db_destroy(db); db = image_db_create(NULL); ASSERT(db != NULL); ASSERT(image_db_get_root(db) != NULL); ASSERT(image_db_is_modified(db)); ASSERT(image_db_get_name(db) == NULL); image_db_set_name(db, TEST_DB_NAME); ASSERT(strcmp(image_db_get_name(db), TEST_DB_NAME) == 0); }); return 0; }