diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6d11057b..f3dfb761 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -63,6 +63,7 @@ set(APPSTREAM_LIB_SRC as-validator-issue.c as-icon.c as-translation.c + as-suggested.c ) set(APPSTREAM_LIB_PUBLIC_HEADERS @@ -84,6 +85,7 @@ set(APPSTREAM_LIB_PUBLIC_HEADERS as-validator-issue.h as-icon.h as-translation.h + as-suggested.h ) set(APPSTREAM_LIB_PRIVATE_HEADERS diff --git a/src/appstream.h b/src/appstream.h index 5017c1a3..b06ea6aa 100644 --- a/src/appstream.h +++ b/src/appstream.h @@ -37,6 +37,7 @@ extern "C" { #include #include #include +#include #include #include diff --git a/src/as-component.c b/src/as-component.c index 12552db7..f3b5eccd 100644 --- a/src/as-component.c +++ b/src/as-component.c @@ -70,6 +70,7 @@ typedef struct GPtrArray *extensions; /* of string */ GPtrArray *screenshots; /* of AsScreenshot elements */ GPtrArray *releases; /* of AsRelease elements */ + GPtrArray *suggestions; /* of AsSuggested elements */ GHashTable *provided; /* of int:object */ GHashTable *urls; /* of int:utf8 */ @@ -119,7 +120,8 @@ enum { AS_COMPONENT_PROJECT_LICENSE, AS_COMPONENT_PROJECT_GROUP, AS_COMPONENT_DEVELOPER_NAME, - AS_COMPONENT_SCREENSHOTS + AS_COMPONENT_SCREENSHOTS, + AS_COMPONENT_SUGGESTIONS }; /** @@ -141,6 +143,7 @@ as_component_kind_get_type (void) {AS_COMPONENT_KIND_INPUTMETHOD, "AS_COMPONENT_KIND_INPUTMETHOD", "inputmethod"}, {AS_COMPONENT_KIND_ADDON, "AS_COMPONENT_KIND_ADDON", "addon"}, {AS_COMPONENT_KIND_FIRMWARE, "AS_COMPONENT_KIND_FIRMWARE", "firmware"}, + {AS_COMPONENT_KIND_MERGE, "AS_COMPONENT_KIND_MERGE", "merge"}, {AS_COMPONENT_KIND_LAST, "AS_COMPONENT_KIND_LAST", "last"}, {0, NULL, NULL} }; @@ -176,6 +179,8 @@ as_component_kind_to_string (AsComponentKind kind) return "addon"; if (kind == AS_COMPONENT_KIND_FIRMWARE) return "firmware"; + if (kind == AS_COMPONENT_KIND_MERGE) + return "merge"; return "unknown"; } @@ -204,6 +209,8 @@ as_component_kind_from_string (const gchar *kind_str) return AS_COMPONENT_KIND_ADDON; if (g_strcmp0 (kind_str, "firmware") == 0) return AS_COMPONENT_KIND_FIRMWARE; + if (g_strcmp0 (kind_str, "merge") == 0) + return AS_COMPONENT_KIND_MERGE; return AS_COMPONENT_KIND_UNKNOWN; } @@ -227,6 +234,7 @@ as_component_init (AsComponent *cpt) priv->screenshots = g_ptr_array_new_with_free_func (g_object_unref); priv->releases = g_ptr_array_new_with_free_func (g_object_unref); + priv->suggestions = g_ptr_array_new_with_free_func (g_object_unref); priv->icons = g_ptr_array_new_with_free_func (g_object_unref); priv->icons_sizetab = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); @@ -268,6 +276,7 @@ as_component_finalize (GObject* object) g_ptr_array_unref (priv->screenshots); g_ptr_array_unref (priv->releases); + g_ptr_array_unref (priv->suggestions); g_hash_table_unref (priv->provided); g_hash_table_unref (priv->urls); g_hash_table_unref (priv->languages); @@ -401,6 +410,22 @@ as_component_add_release (AsComponent *cpt, AsRelease* release) g_ptr_array_add (releases, g_object_ref (release)); } +/** + * as_component_add_suggestion: + * @cpt: a #AsComponent instance. + * @suggested: The #AsSuggested to add + * + * Add an #AsSuggested to this component. + **/ +void +as_component_add_suggestion (AsComponent *cpt, AsSuggested* suggested) +{ + GPtrArray* suggestions; + + suggestions = as_component_get_suggestions (cpt); + g_ptr_array_add (suggestions, g_object_ref (suggested)); +} + /** * as_component_get_urls_table: * @cpt: a #AsComponent instance. @@ -1360,6 +1385,22 @@ as_component_get_compulsory_for_desktops (AsComponent *cpt) return priv->compulsory_for_desktops; } +/** + * as_component_get_suggestions: + * @cpt: a #AsComponent instance. + * + * Get a list of associated suggestions. + * + * Returns: (element-type AsSuggested) (transfer none): an array of #AsSuggested instances + */ +GPtrArray* +as_component_get_suggestions (AsComponent *cpt) +{ + AsComponentPrivate *priv = GET_PRIVATE (cpt); + + return priv->suggestions; +} + /** * as_component_set_compulsory_for_desktops: * @cpt: a #AsComponent instance. @@ -2234,6 +2275,9 @@ as_component_get_property (GObject * object, guint property_id, GValue * value, case AS_COMPONENT_SCREENSHOTS: g_value_set_boxed (value, as_component_get_screenshots (cpt)); break; + case AS_COMPONENT_SUGGESTIONS: + g_value_set_boxed (value, as_component_get_suggestions (cpt)); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; @@ -2411,6 +2455,14 @@ as_component_class_init (AsComponentClass * klass) g_object_class_install_property (object_class, AS_COMPONENT_SCREENSHOTS, g_param_spec_boxed ("screenshots", "screenshots", "screenshots", G_TYPE_PTR_ARRAY, G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB | G_PARAM_READABLE)); + /** + * AsComponent:suggestions: (type GPtrArray(AsSuggested)): + * + * An array of #AsSuggested instances + */ + g_object_class_install_property (object_class, + AS_COMPONENT_SUGGESTIONS, + g_param_spec_boxed ("suggestions", "suggestions", "suggestions", G_TYPE_PTR_ARRAY, G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB | G_PARAM_READABLE)); } /** diff --git a/src/as-component.h b/src/as-component.h index 3eb243fe..7990fc4f 100644 --- a/src/as-component.h +++ b/src/as-component.h @@ -32,6 +32,7 @@ #include "as-screenshot.h" #include "as-release.h" #include "as-translation.h" +#include "as-suggested.h" G_BEGIN_DECLS @@ -60,6 +61,7 @@ struct _AsComponentClass * @AS_COMPONENT_KIND_INPUTMETHOD: An input-method provider * @AS_COMPONENT_KIND_ADDON: An extension of existing software, which does not run standalone * @AS_COMPONENT_KIND_FIRMWARE: Firmware + * @AS_COMPONENT_KIND_MERGE: A component used to provide additional information to an existing component * * The type of an #AsComponent. **/ @@ -72,6 +74,7 @@ typedef enum { AS_COMPONENT_KIND_INPUTMETHOD, AS_COMPONENT_KIND_ADDON, AS_COMPONENT_KIND_FIRMWARE, + AS_COMPONENT_KIND_MERGE, /*< private >*/ AS_COMPONENT_KIND_LAST } AsComponentKind; @@ -155,6 +158,10 @@ GPtrArray *as_component_get_screenshots (AsComponent *cpt); void as_component_add_screenshot (AsComponent *cpt, AsScreenshot *sshot); +GPtrArray *as_component_get_suggestions (AsComponent *cpt); +void as_component_add_suggestion (AsComponent *cpt, + AsSuggested *suggested); + gchar **as_component_get_keywords (AsComponent *cpt); void as_component_set_keywords (AsComponent *cpt, gchar **value, diff --git a/src/as-data-pool.c b/src/as-data-pool.c index 8da0bd9c..aa8d3464 100644 --- a/src/as-data-pool.c +++ b/src/as-data-pool.c @@ -215,10 +215,17 @@ as_data_pool_merge_components (AsDataPool *dpool, AsComponent *src_cpt, AsCompon guint i; gchar **cats; gchar **pkgnames; + GPtrArray* suggestions; + AsComponentKind src_kind; /* FIXME: We only do this for GNOME Software compatibility. In future, we need better rules on what to merge how, and * whether we want to merge stuff at all. */ + src_kind = as_component_get_kind (src_cpt); + + if (src_kind != AS_COMPONENT_KIND_MERGE) + return; + cats = as_component_get_categories (src_cpt); if (cats != NULL) { g_autoptr(GHashTable) cat_table = NULL; @@ -243,6 +250,12 @@ as_data_pool_merge_components (AsDataPool *dpool, AsComponent *src_cpt, AsCompon if ((pkgnames != NULL) && (pkgnames[0] != '\0')) as_component_set_pkgnames (dest_cpt, as_component_get_pkgnames (src_cpt)); + suggestions = as_component_get_suggestions (src_cpt); + if (suggestions != NULL) { + for (i = 0; i < suggestions->len; i++) + as_component_add_suggestion (dest_cpt, g_ptr_array_index (suggestions, i)); + } + if (as_component_has_bundle (src_cpt)) as_component_set_bundles_table (dest_cpt, as_component_get_bundles_table (src_cpt)); } diff --git a/src/as-suggested.c b/src/as-suggested.c new file mode 100644 index 00000000..17bb0057 --- /dev/null +++ b/src/as-suggested.c @@ -0,0 +1,214 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2016 Lucas Moura + * + * Licensed under the GNU Lesser General Public License Version 2.1 + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 of the license, or + * (at your option) any later version. + * + * This library 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ + +/** + * SECTION: as-suggested + * @short_description: Descibe which componentes were responsible for + * a given package recommendation. + * @include: appstream.h + */ + +#include "config.h" + +#include "as-suggested.h" + +typedef struct +{ + AsSuggestedKind kind; + GPtrArray *cpts_id; /* of string */ +} AsSuggestedPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (AsSuggested, as_suggested, G_TYPE_OBJECT) +#define GET_PRIVATE(o) (as_suggested_get_instance_private (o)) + +/** + * as_suggested_kind_to_string: + * @kind: the %AsSuggestedKind. + * + * Converts the enumerated value to an text representation. + * + * Returns: string version of @kind + **/ +const gchar* +as_suggested_kind_to_string (AsSuggestedKind kind) +{ + if (kind == AS_SUGGESTED_KIND_HEURISTIC) + return "heuristic"; + if (kind == AS_SUGGESTED_KIND_UPSTREAM) + return "upstream"; + + return "unknown"; +} + +/** + * as_suggested_kind_from_string: + * @kind_str: the string. + * + * Converts the text representation to an enumerated value. + * + * Returns: a #AsSuggestedKind or %AS_SUGGESTED_KIND_UNKNOWN for unknown + **/ +AsSuggestedKind +as_suggested_kind_from_string (const gchar *kind_str) +{ + if (g_strcmp0 (kind_str, "heuristic") == 0) + return AS_SUGGESTED_KIND_HEURISTIC; + if (g_strcmp0 (kind_str, "upstream") == 0) + return AS_SUGGESTED_KIND_UPSTREAM; + + return AS_SUGGESTED_KIND_UNKNOWN; +} + +/** + * as_suggested_finalize: + **/ +static void +as_suggested_finalize (GObject *object) +{ + AsSuggested *suggested = AS_SUGGESTED (object); + AsSuggestedPrivate *priv = GET_PRIVATE (suggested); + + g_ptr_array_unref (priv->cpts_id); + + G_OBJECT_CLASS (as_suggested_parent_class)->finalize (object); +} + +/** + * as_suggested_init: + **/ +static void +as_suggested_init (AsSuggested *suggested) +{ + AsSuggestedPrivate *priv = GET_PRIVATE (suggested); + + priv->cpts_id = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); +} + +/** + * as_suggested_new: + * + * Creates a new #AsSuggested. + * + * Returns: (transfer full): a new #AsSuggested + **/ +AsSuggested* +as_suggested_new (void) +{ + AsSuggested *suggested; + suggested = g_object_new (AS_TYPE_SUGGESTED, NULL); + return AS_SUGGESTED (suggested); +} + +/** + * as_suggested_class_init: + **/ +static void +as_suggested_class_init (AsSuggestedClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = as_suggested_finalize; +} + +/** + * as_suggested_get_kind: + * @suggested: a #AsSuggested instance. + * + * Gets the suggested kind. + * + * Returns: the #AssuggestedKind + **/ +AsSuggestedKind +as_suggested_get_kind (AsSuggested *suggested) +{ + AsSuggestedPrivate *priv = GET_PRIVATE (suggested); + return priv->kind; +} + +/** + * as_suggested_set_kind: + * @suggested: a #AsSuggested instance. + * @kind: the #AsSuggestedKind, e.g. %AS_SUGGESTED_KIND_HEURISTIC. + * + * Sets the suggested kind. + **/ +void +as_suggested_set_kind (AsSuggested *suggested, AsSuggestedKind kind) +{ + AsSuggestedPrivate *priv = GET_PRIVATE (suggested); + priv->kind = kind; +} + +/** + * as_suggested_get_components_id: + * @suggested: a #AsSuggested instance. + * + * Get a list of components id that generated the suggestion + * + * Returns: an array of components id + */ +GPtrArray* +as_suggested_get_components_id (AsSuggested* suggested) +{ + AsSuggestedPrivate *priv = GET_PRIVATE (suggested); + + return priv->cpts_id; +} + + +/** + * as_suggested_add_component_id: + * @suggested: a #AsSuggested instance. + * @cpt_id: The component id to add + * + * Add a component id to this suggested object. + **/ +void +as_suggested_add_component_id (AsSuggested *suggested, gchar* cpt_id) +{ + AsSuggestedPrivate *priv = GET_PRIVATE (suggested); + g_ptr_array_add (priv->cpts_id, cpt_id); +} + +/** + * as_suggested_is_valid: + * @suggested: a #AsSuggested instance. + * + * Check if the essential properties of this suggestion are + * populated with useful data. + * + * Returns: TRUE if the suggestion data was validated successfully. + */ +gboolean +as_suggested_is_valid (AsSuggested *suggested) +{ + gboolean ret = FALSE; + AsSuggestedKind stype; + + AsSuggestedPrivate *priv = GET_PRIVATE (suggested); + + stype = priv->kind; + if (stype == AS_SUGGESTED_KIND_UNKNOWN) + return FALSE; + + if (priv->cpts_id->len != 0) + ret = TRUE; + + return ret; +} diff --git a/src/as-suggested.h b/src/as-suggested.h new file mode 100644 index 00000000..42569d90 --- /dev/null +++ b/src/as-suggested.h @@ -0,0 +1,79 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2016 Lucas Moura + * + * Licensed under the GNU Lesser General Public License Version 2.1 + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 of the license, or + * (at your option) any later version. + * + * This library 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ + +#if !defined (__APPSTREAM_H) && !defined (AS_COMPILATION) +#error "Only can be included directly." +#endif + +#ifndef __AS_SUGGESTED_H +#define __AS_SUGGESTED_H + +#include + +G_BEGIN_DECLS + +#define AS_TYPE_SUGGESTED (as_suggested_get_type ()) +G_DECLARE_DERIVABLE_TYPE (AsSuggested, as_suggested, AS, SUGGESTED, GObject) + +struct _AsSuggestedClass +{ + GObjectClass parent_class; + /*< private >*/ + void (*_as_reserved1) (void); + void (*_as_reserved2) (void); + void (*_as_reserved3) (void); + void (*_as_reserved4) (void); + void (*_as_reserved5) (void); + void (*_as_reserved6) (void); +}; + +/** + * AsSuggestedKind: + * @as_suggested_KIND_UNKNOWN: Unknown suggested kind + * @AS_SUGGESTED_KIND_HEURISTIC: suggested created by a recommender application + * + * The suggested type. + **/ +typedef enum { + AS_SUGGESTED_KIND_UNKNOWN, + AS_SUGGESTED_KIND_HEURISTIC, + AS_SUGGESTED_KIND_UPSTREAM, + /*< private >*/ + AS_SUGGESTED_KIND_LAST +} AsSuggestedKind; + +AsSuggestedKind as_suggested_kind_from_string (const gchar *kind_str); +const gchar *as_suggested_kind_to_string (AsSuggestedKind kind); + +AsSuggested *as_suggested_new (void); + +AsSuggestedKind as_suggested_get_kind (AsSuggested *suggested); +void as_suggested_set_kind (AsSuggested *suggested, + AsSuggestedKind kind); + +GPtrArray* as_suggested_get_components_id (AsSuggested *suggested); +void as_suggested_add_component_id (AsSuggested *suggested, gchar* cpt_id); + +gboolean as_suggested_is_valid (AsSuggested *suggested); + + +G_END_DECLS + +#endif /* __AS_SUGGESTED_H */ diff --git a/src/as-xmldata.c b/src/as-xmldata.c index 74fdeb60..8cc844ec 100644 --- a/src/as-xmldata.c +++ b/src/as-xmldata.c @@ -442,6 +442,50 @@ as_xmldata_process_screenshots_tag (AsXMLData *xdt, xmlNode* node, AsComponent* } } +/** + * as_xmldata_process_suggests_tag: + */ +static void +as_xmldata_process_suggests_tag (AsXMLData *xdt, xmlNode* node, AsComponent* cpt) +{ + xmlNode *iter; + AsSuggested *suggested = NULL; + AsSuggestedKind suggested_kind; + gchar *node_name; + gchar *type_str; + gchar *content; + + g_return_if_fail (xdt != NULL); + g_return_if_fail (cpt != NULL); + + suggested = as_suggested_new (); + type_str = (gchar*) xmlGetProp (node, (xmlChar*) "type"); + + if (type_str != NULL) { + suggested_kind = as_suggested_kind_from_string (type_str); + as_suggested_set_kind (suggested, suggested_kind); + } + + for (iter = node->children; iter != NULL; iter = iter->next) { + /* discard spaces */ + if (iter->type != XML_ELEMENT_NODE) + continue; + + node_name = (gchar*) iter->name; + + if (g_strcmp0 (node_name, "id") == 0) { + content = as_xmldata_get_node_value (xdt, iter); + + if (content != NULL) + as_suggested_add_component_id (suggested, content); + } + + if (as_suggested_is_valid (suggested)) + as_component_add_suggestion (cpt, suggested); + + } +} + /** * as_xmldata_upstream_description_to_cpt: * @@ -940,6 +984,8 @@ as_xmldata_parse_component_node (AsXMLData *xdt, xmlNode* node, AsComponent *cpt as_xmldata_process_provides (xdt, iter, cpt); } else if (g_strcmp0 (node_name, "screenshots") == 0) { as_xmldata_process_screenshots_tag (xdt, iter, cpt); + } else if (g_strcmp0 (node_name, "suggests") == 0) { + as_xmldata_process_suggests_tag (xdt, iter, cpt); } else if (g_strcmp0 (node_name, "project_license") == 0) { if (content != NULL) as_component_set_project_license (cpt, content); diff --git a/tests/samples/distro/xmls/suggestions.xml b/tests/samples/distro/xmls/suggestions.xml new file mode 100644 index 00000000..44d2273b --- /dev/null +++ b/tests/samples/distro/xmls/suggestions.xml @@ -0,0 +1,31 @@ + + + + test.desktop + test + Test + + test1.desktop + test2.desktop + + + + test1.desktop + test + Test + + + test1.desktop + + test5.desktop + test6.desktop + + + + test1.desktop + + test7.desktop + test8.desktop + + + diff --git a/tests/test-pool.c b/tests/test-pool.c index 620badb9..a01d67ed 100644 --- a/tests/test-pool.c +++ b/tests/test-pool.c @@ -193,6 +193,70 @@ test_pool_read () g_assert_cmpuint (as_release_get_size (rel, AS_SIZE_KIND_DOWNLOAD), ==, 0); } +void +test_merge_components () +{ + guint i; + + g_autoptr(GPtrArray) cpts = NULL; + g_autoptr(AsDataPool) dpool = NULL; + g_autoptr(GError) error = NULL; + g_auto(GStrv) strv = NULL; + g_auto(GStrv) expected_cpts_id = NULL; + g_autoptr(GPtrArray) suggestions = NULL; + + GPtrArray* cpts_id = NULL; + AsComponent *cpt = NULL; + AsSuggested *suggested = NULL; + AsSuggestedKind suggested_kind; + gchar *cpt_id = NULL; + + dpool = as_data_pool_new (); + + strv = g_new0 (gchar*, 2); + strv[0] = g_build_filename (datadir, "distro", NULL); + + as_data_pool_set_metadata_locations (dpool, strv); + as_data_pool_load_metadata (dpool); + + cpt = as_data_pool_get_component_by_id (dpool, "test.desktop"); + suggestions = as_component_get_suggestions (cpt); + suggested = (AsSuggested *) g_ptr_array_index (suggestions, 0); + cpts_id = as_suggested_get_components_id (suggested); + suggested_kind = as_suggested_get_kind (suggested); + + expected_cpts_id = g_new0 (gchar*, 3); + expected_cpts_id[0] = g_strdup ("test1.desktop"); + expected_cpts_id[1] = g_strdup ("test2.desktop"); + + g_assert_cmpint (suggestions->len, ==, 2); + g_assert_cmpint (suggested_kind, ==, AS_SUGGESTED_KIND_HEURISTIC); + + for (i = 0; i < cpts_id->len; i++) { + cpt_id = (gchar*) g_ptr_array_index (cpts_id, i); + g_assert_cmpstr (cpt_id, ==, expected_cpts_id[i]); + } + + cpt = as_data_pool_get_component_by_id (dpool, "test1.desktop"); + suggestions = as_component_get_suggestions (cpt); + suggested = (AsSuggested *) g_ptr_array_index (suggestions, 0); + cpts_id = as_suggested_get_components_id (suggested); + suggested_kind = as_suggested_get_kind (suggested); + + expected_cpts_id = g_new0 (gchar*, 3); + expected_cpts_id[0] = g_strdup ("test5.desktop"); + expected_cpts_id[1] = g_strdup ("test6.desktop"); + + g_assert_cmpint (suggestions->len, ==, 2); + g_assert_cmpint (suggested_kind, ==, AS_SUGGESTED_KIND_HEURISTIC); + + for (i = 0; i < cpts_id->len; i++) { + cpt_id = (gchar*) g_ptr_array_index (cpts_id, i); + g_assert_cmpstr (cpt_id, ==, expected_cpts_id[i]); + } + +} + int main (int argc, char **argv) { @@ -216,6 +280,7 @@ main (int argc, char **argv) g_test_add_func ("/AppStream/Cache", test_cache); g_test_add_func ("/AppStream/PoolRead", test_pool_read); + g_test_add_func ("/AppStream/ComponentMerge", test_merge_components); ret = g_test_run (); g_free (datadir);