1 /*
2 
3    xlog - GTK+ logging program for amateur radio operators
4    Copyright (C) 2001 - 2008 Joop Stakenborg <pg4i@amsat.org>
5 
6    This file is part of xlog.
7 
8    Xlog is free software: you can redistribute it and/or modify
9    it under the terms of the GNU General Public License as published by
10    the Free Software Foundation, either version 3 of the License, or
11    (at your option) any later version.
12 
13    Xlog is distributed in the hope that it will be useful,
14    but WITHOUT ANY WARRANTY; without even the implied warranty of
15    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16    GNU General Public License for more details.
17 
18    You should have received a copy of the GNU General Public License
19    along with xlog.  If not, see <http://www.gnu.org/licenses/>.
20 
21 */
22 
23 /*
24  * log.c - assorted utilities for maintaining the logs
25  */
26 
27 #include <gtk/gtk.h>
28 #include <unistd.h>
29 #include <stdlib.h>
30 #include <errno.h>
31 #include <string.h>
32 #include <sys/types.h>
33 #include <glib/gstdio.h>
34 
35 #ifndef G_OS_WIN32
36 #include <sys/wait.h>
37 #endif
38 
39 #include "callbacks_mainwindow_list.h"
40 #include "gui_utils.h"
41 #include "utils.h"
42 #include "cfg.h"
43 #include "log.h"
44 #include "support.h"
45 #include "main.h"
46 #include "dxcc.h"
47 
48 extern GtkWidget *mainnotebook;
49 extern programstatetype programstate;
50 extern preferencestype preferences;
51 extern gchar **qso;
52 
53 /* backup a log */
54 void
backuplog(gchar * filename,gchar * backupfilename)55 backuplog (gchar * filename, gchar * backupfilename)
56 {
57 	gint ch;
58 	gchar *msg;
59 	FILE *in, *out;
60 
61 	in = g_fopen (filename, "r");
62 	if (in)
63 	{
64 		out = g_fopen (backupfilename, "w");
65 		if (out)
66 		{
67 			while ((ch = getc (in)) != EOF)
68 				putc (ch, out);
69 			fclose (out);
70 		}
71 		else
72 		{
73 			msg = g_strdup_printf (_("Backup to %s failed: %s"),
74 				backupfilename, g_strerror (errno));
75 			g_warning ("%s", msg);
76 			g_free (msg);
77 		}
78 		fclose (in);
79 	}
80 }
81 
82 /* extract name of the log from filename, returned string should be free'd */
83 gchar *
logname(gchar * filename)84 logname (gchar * filename)
85 {
86 	gchar *logname, *basen, **split;
87 
88 	basen = g_path_get_basename (filename);
89 	split = g_strsplit (basen, ".", -1);
90 	logname = g_strdup (split[0]);
91 	g_free (basen);
92 	g_strfreev (split);
93 	return (logname);
94 }
95 
96 /*
97  * Prepend qsos to the log, we convert from locale to UTF-8 here. If for some
98  * reason locale conversion fails we set a flag and replace the character
99  * with a dot. If that fails, the field is emptied. The flag is used to display
100  * a warning dialog. Only date, name, QTH, freefield1, freefield2 and the
101  * remarks field are checked for locales.
102  */
103 gint
fillin_list(LOGDB * handle,qso_t q[],gpointer arg)104 fillin_list (LOGDB * handle, qso_t q[], gpointer arg)
105 {
106 	GtkTreeIter iter;
107 	GtkListStore *model;
108 	logtype *logw = (logtype *) arg;
109 	gchar *date = NULL, *name = NULL, *qth = NULL, *u1 = NULL,
110 		*u2 = NULL, *remarks = NULL;
111 	GError *error;
112 	gint i;
113 	GtkTreePath *path;
114 
115 	programstate.qsos++;
116 	logw->qsos++;
117 	q[NR] = g_strdup_printf ("%d", logw->qsos);
118 
119 	model = GTK_LIST_STORE
120 		(gtk_tree_view_get_model (GTK_TREE_VIEW (logw->treeview)));
121 	gtk_list_store_prepend (GTK_LIST_STORE (model), &iter);
122 
123 	if (!q[DATE])
124 		date = g_strdup ("");
125 	else if (!g_utf8_validate (q[DATE], -1, NULL ))
126 	{
127 	  error = NULL;
128 		date = g_locale_to_utf8 (q[DATE], -1, NULL, NULL, &error);
129 		if (!date)
130 		{
131 			g_warning (_("Unable to convert '%s' to UTF-8: %s"), q[DATE], error->message);
132 			g_error_free (error);
133 			programstate.utf8error = TRUE;
134 			date = g_convert_with_fallback(q[DATE], strlen(q[DATE]), "UTF-8", "ISO-8859-1", ".", NULL, NULL, NULL);
135 			if (!date) date = g_strdup ("");
136 		}
137  	}
138 	else date = g_strdup (q[DATE]);
139 
140 	if (!q[GMT])
141 		q[GMT] = g_strdup ("");
142 	if (!q[GMTEND])
143 		q[GMTEND] = g_strdup ("");
144 	if (!q[CALL])
145 		q[CALL] = g_strdup ("");
146 	if (!q[BAND])
147 		q[BAND] = g_strdup ("");
148 	if (!q[MODE])
149 		q[MODE] = g_strdup ("");
150 	if (!q[RST])
151 		q[RST] = g_strdup ("");
152 	if (!q[MYRST])
153 		q[MYRST] = g_strdup ("");
154 	if (!q[AWARDS])
155 		q[AWARDS] = g_strdup ("");
156 	if (!q[QSLOUT])
157 		q[QSLOUT] = g_strdup ("");
158 	if (!q[QSLIN])
159 		q[QSLIN] = g_strdup ("");
160 	if (!q[POWER])
161 		q[POWER] = g_strdup ("");
162 
163 	if (!q[NAME])
164 		name = g_strdup ("");
165 	else if (!g_utf8_validate (q[NAME], -1, NULL ))
166 	{
167 	  error = NULL;
168 		name = g_locale_to_utf8 (q[NAME], -1, NULL, NULL, &error);
169 		if (!name)
170 		{
171 			g_warning (_("Unable to convert '%s' to UTF-8: %s"), q[NAME], error->message);
172 			g_error_free (error);
173 			programstate.utf8error = TRUE;
174 			name = g_convert_with_fallback(q[NAME], strlen(q[NAME]), "UTF-8", "ISO-8859-1", ".", NULL, NULL, NULL);
175 			if (!name) name = g_strdup ("");
176 		}
177  	}
178 	else name = g_strdup (q[NAME]);
179 
180 	if (!q[QTH])
181 		qth = g_strdup ("");
182 	else if (!g_utf8_validate (q[QTH], -1, NULL ))
183 	{
184 	  error = NULL;
185 		qth = g_locale_to_utf8 (q[QTH], -1, NULL, NULL, &error);
186 		if (!qth)
187 		{
188 			g_warning (_("Unable to convert '%s' to UTF-8: %s"), q[QTH], error->message);
189 			g_error_free (error);
190 			programstate.utf8error = TRUE;
191 			qth = g_convert_with_fallback(q[QTH], strlen(q[QTH]), "UTF-8", "ISO-8859-1", ".", NULL, NULL, NULL);
192 			if (!qth) qth = g_strdup ("");
193 		}
194  	}
195 	else qth = g_strdup (q[QTH]);
196 
197 	if (!q[LOCATOR])
198 		q[LOCATOR] = g_strdup ("");
199 
200 	if (!q[U1])
201 		u1 = g_strdup ("");
202 	else if (!g_utf8_validate (q[U1], -1, NULL ))
203 	{
204 	  error = NULL;
205 		u1 = g_locale_to_utf8 (q[U1], -1, NULL, NULL, &error);
206 		if (!u1)
207 		{
208 			g_warning (_("Unable to convert '%s' to UTF-8: %s"), q[U1], error->message);
209 			g_error_free (error);
210 			programstate.utf8error = TRUE;
211 			u1 = g_convert_with_fallback(q[U1], strlen(q[U1]), "UTF-8", "ISO-8859-1", ".", NULL, NULL, NULL);
212 			if (!u1) u1 = g_strdup ("");
213 		}
214  	}
215 	else u1 = g_strdup (q[U1]);
216 
217 	if (!q[U2])
218 		u2 = g_strdup ("");
219 	else if (!g_utf8_validate (q[U2], -1, NULL ))
220 	{
221 	  error = NULL;
222 		u2 = g_locale_to_utf8 (q[U2], -1, NULL, NULL, &error);
223 		if (!u2)
224 		{
225 			g_warning (_("Unable to convert '%s' to UTF-8: %s"), q[U2], error->message);
226 			g_error_free (error);
227 			programstate.utf8error = TRUE;
228 			u2 = g_convert_with_fallback(q[U2], strlen(q[U2]), "UTF-8", "ISO-8859-1", ".", NULL, NULL, NULL);
229 			if (!u2) u2 = g_strdup ("");
230 		}
231  	}
232 	else u2 = g_strdup (q[U2]);
233 
234 	if (!q[REMARKS])
235 		remarks = g_strdup ("");
236 	else if (!g_utf8_validate (q[REMARKS], -1, NULL ))
237 	{
238 	  error = NULL;
239 		remarks = g_locale_to_utf8 (q[REMARKS], -1, NULL, NULL, &error);
240 		if (!remarks)
241 		{
242 			g_warning (_("Unable to convert '%s' to UTF-8: %s"), q[REMARKS], error->message);
243 			g_error_free (error);
244 			programstate.utf8error = TRUE;
245 			remarks = g_convert_with_fallback(q[REMARKS], strlen(q[REMARKS]), "UTF-8", "ISO-8859-1", ".", NULL, NULL, NULL);
246 			if (!remarks) remarks = g_strdup ("");
247 		}
248  	}
249 	else remarks = g_strdup (q[REMARKS]);
250 
251 	gtk_list_store_set (GTK_LIST_STORE (model), &iter,
252 		NR, q[NR], DATE, date, GMT, q[GMT], GMTEND, q[GMTEND], CALL, q[CALL],
253 		BAND, q[BAND], MODE, q[MODE], RST, q[RST], MYRST, q[MYRST], AWARDS, q[AWARDS],
254 		QSLOUT, q[QSLOUT], QSLIN, q[QSLIN], POWER, q[POWER], NAME, name, QTH, qth,
255 		LOCATOR, q[LOCATOR], U1, u1, U2, u2, REMARKS, remarks, -1);
256 	if (!(logw->qsos % 1000))
257 	{
258 		while (gtk_events_pending ())
259 			gtk_main_iteration ();
260 
261 		path = gtk_tree_path_new_from_string ("0");
262 		gtk_tree_view_scroll_to_cell
263 			(GTK_TREE_VIEW (logw->treeview), path, NULL, TRUE, 0.5, 0.0);
264 		gtk_tree_path_free (path);
265 
266 		update_statusbar (_("Reading..."));
267 	}
268 
269 	for (i = 0; i < QSO_FIELDS; i++)
270 		g_free (q[i]);
271 
272 	if (date) g_free (date);
273 	if (remarks) g_free (remarks);
274 	if (name) g_free (name);
275 	if (qth) g_free (qth);
276 	if (u1) g_free (u1);
277 	if (u2) g_free (u2);
278 	return 0;
279 }
280 
281 /* create a new struct for a log */
282 static logtype *
new_logwindow(void)283 new_logwindow (void)
284 {
285 	logtype *newlog;
286 	gint i;
287 
288 	newlog = g_new0 (struct logtype, 1);
289 	newlog->scrolledwindow = NULL;
290 	newlog->treeview = NULL;
291 	newlog->label = NULL;
292 	newlog->logname = NULL;
293 	newlog->filename = NULL;
294 	newlog->logchanged = FALSE;
295 	newlog->readonly = FALSE;
296 	newlog->qsos = 0;
297 	newlog->columns = 0;
298 	for (i = 0; i < QSO_FIELDS; i++)
299 		newlog->logfields[i] = 0;
300 	return (newlog);
301 }
302 
303 
304 /* open a log and return a struct */
305 logtype *
openlogwindow(LOGDB * lp,gchar * name,gint page)306 openlogwindow (LOGDB * lp, gchar * name, gint page)
307 {
308 	logtype *logwindow;
309 	gint i, j;
310 	gchar *logn, *labelname;
311 	GtkCellRenderer *renderer, *brenderer;
312 	GtkTreeViewColumn *column;
313 	GObject *selection;
314 	GtkListStore *model;
315 
316 	logwindow = new_logwindow ();
317 	logwindow->scrolledwindow = gtk_scrolled_window_new (NULL, NULL);
318 	gtk_widget_show (logwindow->scrolledwindow);
319 	gtk_container_add (GTK_CONTAINER (mainnotebook), logwindow->scrolledwindow);
320 	gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW(logwindow->scrolledwindow),
321 		GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
322 
323 	model = gtk_list_store_new (QSO_FIELDS,
324 		G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
325 		G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
326 		G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
327 		G_TYPE_STRING);
328 	logwindow->treeview = gtk_tree_view_new_with_model (GTK_TREE_MODEL (model));
329 	gtk_tree_view_set_search_column (GTK_TREE_VIEW (logwindow->treeview), -1);
330 	g_object_unref (G_OBJECT (model));
331 
332 	/* add callback for selecting a row */
333 	selection = G_OBJECT (gtk_tree_view_get_selection(GTK_TREE_VIEW (logwindow->treeview)));
334 	gtk_tree_selection_set_mode (GTK_TREE_SELECTION (selection), GTK_SELECTION_SINGLE);
335 	g_signal_connect (selection, "changed",	G_CALLBACK (on_log_select_row), NULL);
336 
337 	logwindow->columns = lp->column_nr;
338 	/* save the active columns */
339 	for (j = 0; j < logwindow->columns; j++)
340 		logwindow->logfields[j] = lp->column_fields[j];
341 
342 	/* NR column is the first one */
343 	renderer = gtk_cell_renderer_text_new ();
344 	brenderer = gtk_cell_renderer_text_new ();
345 	// AndyS: comment out this line to fix GLib-GObject-WARNING message when xlog starts
346 	// g_object_set (G_OBJECT (brenderer), "weight", "bold", NULL);
347 	column = gtk_tree_view_column_new_with_attributes ("NR", renderer, "text", NR, NULL);
348 	gtk_tree_view_column_set_sizing (GTK_TREE_VIEW_COLUMN (column), GTK_TREE_VIEW_COLUMN_FIXED);
349 	gtk_tree_view_column_set_resizable (GTK_TREE_VIEW_COLUMN (column), TRUE);
350 	gtk_tree_view_append_column (GTK_TREE_VIEW (logwindow->treeview), column);
351 	if (preferences.logcwidths2[NR] == 0)
352 		gtk_tree_view_column_set_visible (GTK_TREE_VIEW_COLUMN(column), FALSE);
353 	else
354 		gtk_tree_view_column_set_fixed_width (GTK_TREE_VIEW_COLUMN(column), preferences.logcwidths2[NR]);
355 
356 	/* see which fields are in the log and add a column or hide it */
357 	for (j = 1; j < QSO_FIELDS; j++)
358 	{
359 		for (i = 0; i < lp->column_nr; i++)
360 		{
361 			if (j == lp->column_fields[i])
362 				break;
363 		}
364 		renderer = gtk_cell_renderer_text_new ();
365 		if (j == U1)
366 			column = gtk_tree_view_column_new_with_attributes
367 				(preferences.freefield1, renderer, "text", j, NULL);
368 		else if (j == U2)
369 			column = gtk_tree_view_column_new_with_attributes
370 				(preferences.freefield2, renderer, "text", j, NULL);
371 		else if (j == CALL)
372 			column = gtk_tree_view_column_new_with_attributes
373 				(strcolumn (j), brenderer, "text", j, NULL);
374 		else
375 			column = gtk_tree_view_column_new_with_attributes
376 				(strcolumn (j), renderer, "text", j, NULL);
377 
378 		gtk_tree_view_column_set_sizing
379 			(GTK_TREE_VIEW_COLUMN(column), GTK_TREE_VIEW_COLUMN_FIXED);
380 		gtk_tree_view_column_set_resizable
381 			(GTK_TREE_VIEW_COLUMN(column), TRUE);
382 		gtk_tree_view_column_set_fixed_width
383 			(GTK_TREE_VIEW_COLUMN(column), preferences.logcwidths2[j]);
384 		gtk_tree_view_append_column (GTK_TREE_VIEW(logwindow->treeview), column);
385 
386 		if (i == lp->column_nr)
387 			gtk_tree_view_column_set_visible (GTK_TREE_VIEW_COLUMN(column), FALSE);
388 	}
389 
390 	g_object_set(G_OBJECT(logwindow->treeview), "fixed-height-mode", TRUE, NULL);
391 	gtk_widget_show (logwindow->treeview);
392 	gtk_container_add (GTK_CONTAINER (logwindow->scrolledwindow), logwindow->treeview);
393 
394 	/* change the page label */
395 	logn = logname (name);
396 	logwindow->label = gtk_label_new (NULL);
397 	labelname = g_strdup_printf ("<b>%s</b>", logn);
398 	gtk_label_set_markup (GTK_LABEL (logwindow->label), labelname);
399 	gtk_widget_show (logwindow->label);
400 	g_free (labelname);
401 	gtk_notebook_set_tab_label (GTK_NOTEBOOK (mainnotebook), gtk_notebook_get_nth_page (GTK_NOTEBOOK(mainnotebook), page), logwindow->label);
402 	gtk_misc_set_padding (GTK_MISC (logwindow->label), 10, 0);
403 	logwindow->logname = g_strdup (logn);
404 	g_free (logn);
405 
406 	return (logwindow);
407 }
408 
409 /* close child process when finished */
410 #ifndef G_OS_WIN32
411 static gboolean
childcheck(void)412 childcheck (void)
413 {
414   gint status, childpid;
415 
416   childpid = waitpid (-1, &status, WNOHANG);
417   return (WIFEXITED(status) == 0);
418 }
419 #endif
420 
421 typedef gchar *item_t[QSO_FIELDS];
savelog_compar_groupbycall(const void * b,const void * a)422 static int savelog_compar_groupbycall(const void *b, const void *a)
423 {
424 	const gchar **item_a, **item_b;
425 	item_a = (const gchar **)a;
426 	item_b = (const gchar **)b;
427 
428 	return strcmp(item_a[CALL], item_b[CALL]);
429 }
430 
431 
432 extern GPtrArray *dxcc;
savelog_compar_sortbydxcc(const void * b,const void * a)433 static int savelog_compar_sortbydxcc(const void *b, const void *a)
434 {
435 	gint rescmp;
436 	gchar **item_a, **item_b;
437 	item_a = (gchar **)a;
438 	item_b = (gchar **)b;
439 
440 	struct info info_a, info_b;
441 	info_a = lookupcountry_by_callsign (item_a[CALL]);
442 	info_b = lookupcountry_by_callsign (item_b[CALL]);
443 	dxcc_data *d_a = g_ptr_array_index (dxcc, info_a.country);
444 	dxcc_data *d_b = g_ptr_array_index (dxcc, info_b.country);
445 
446 	/* Sort by DXCC first, then group by call sign within same DXCC */
447 	rescmp = strcmp(d_b->px, d_a->px);
448 	if (rescmp != 0)
449 		return rescmp;
450 	return strcmp(item_a[CALL], item_b[CALL]);
451 }
452 
453 /* saving of the log */
454 void
savelog(gpointer arg,gchar * logfile,gint type,gint first,gint last)455 savelog (gpointer arg, gchar * logfile, gint type, gint first, gint last)
456 {
457 	LOGDB *lp;
458 	gint i, j, k, pid, exported;
459 	const gchar *label;
460 	gchar *pathstr;
461 	item_t *sorteditems;
462 	gint fields[QSO_FIELDS], widths[QSO_FIELDS];
463 	logtype *logw = (logtype *) arg;
464 	GtkTreeViewColumn *column;
465 	GtkTreeModel *model;
466 	GtkTreePath *path;
467 	GtkTreeIter iter;
468 
469 	/* how many columns do we have and what are the labels, skip first field */
470 	/* NOTE: unknown fields have a fixed label */
471 	for (i = 0; i < logw->columns; i++)
472 	{
473 		column = gtk_tree_view_get_column (GTK_TREE_VIEW (logw->treeview), logw->logfields[i]);
474 		label = gtk_tree_view_column_get_title (column);
475 		if (logw->logfields[i] == U1)
476 			fields[i] = U1;
477 		else if (logw->logfields[i] == U2)
478 			fields[i] = U2;
479 		else
480 			fields[i] = parse_column_name (label);
481 		widths[i] = parse_field_width (fields[i]);
482 	}
483 
484 #ifndef G_OS_WIN32
485 	pid = fork (); /* use fork for log saving */
486 	if (pid == -1)
487 		g_warning(_("fork failed when saving log: %s"), g_strerror (errno));
488 #else
489 	pid = 0;
490 #endif
491 
492 	if (pid == 0)
493 	{ /* this is the child */
494         exported = last - first + 1;
495         save_num_qsos_to_export(exported); /* saved for EDI export, nobody else needs it for now */
496 		lp = log_file_create (logfile, type, logw->columns, fields, widths);
497 		k = 0;
498 		if (lp)
499 		{
500 			model = gtk_tree_view_get_model (GTK_TREE_VIEW(logw->treeview));
501 			/* create an array to be used for sorting */
502 			sorteditems = g_new0 (item_t, exported);
503 			/* go through the QSO's and store in the sort array */
504 			for (i = logw->qsos - first; i >= logw->qsos - last; i--)
505 			{
506 				pathstr = g_strdup_printf ("%d", i);
507 				path = gtk_tree_path_new_from_string (pathstr);
508 				gtk_tree_model_get_iter (model, &iter, path);
509 				for (j = 0; j < logw->columns; j++)
510 				{
511 					gtk_tree_model_get (model, &iter, logw->logfields[j], &sorteditems[k][fields[j]], -1);
512 					if (fields[j] == DATE || fields[j] == NAME || fields[j] == QTH || fields[j] == U1 || fields[j] == U2 || fields[j] == REMARKS)
513 						sorteditems[k][fields[j]] = g_locale_from_utf8 (sorteditems[k][fields[j]], -1, NULL, NULL, NULL);
514 				}
515 				k++;
516 				gtk_tree_path_free (path);
517 				g_free (pathstr);
518 			}
519 
520 			if (type == TYPE_LABELS)
521 			{
522 				if (preferences.tsvsortbydxcc)
523 					qsort(sorteditems, exported, sizeof(gchar*)*QSO_FIELDS, &savelog_compar_sortbydxcc);
524 				else if (preferences.tsvgroupbycallsign > 1)
525 					qsort(sorteditems, exported, sizeof(gchar*)*QSO_FIELDS, &savelog_compar_groupbycall);
526 			}
527 
528 			/* QSO's have been sorted (or not), now save them */
529 
530 			for (i = 0; i <= exported -1; i++)
531 				log_file_qso_append (lp, sorteditems[i]);
532 			/* free the sortarray */
533 			g_free(sorteditems);
534 
535 			log_file_close (lp);
536 #ifndef G_OS_WIN32
537 			_exit (0);
538 #else
539 			return;
540 #endif
541 		}
542 	}
543 	/* parent running */
544 #ifndef G_OS_WIN32
545 	g_timeout_add (1000, (GSourceFunc) childcheck, NULL);
546 #endif
547 }
548 
549 
550 /* look for logs in dir */
551 GPtrArray*
getxlogs(gchar * path,gchar * patt)552 getxlogs (gchar *path, gchar *patt)
553 {
554 	GError *error  = NULL;
555 
556  	GDir *dir = g_dir_open (path, 0, &error);
557 	gchar *pattern = g_strdup_printf ("%s.xlog", patt);
558  	GPtrArray *arr = g_ptr_array_new ();
559 	if (!error)
560 	{
561 		const gchar *dirname = g_dir_read_name (dir);
562 		while (dirname)
563 		{
564 			if (g_pattern_match_simple (pattern, dirname))
565 				g_ptr_array_add (arr, g_strdup(dirname));
566 			dirname = g_dir_read_name (dir);
567 		}
568 		g_dir_close (dir);
569 	}
570 	g_free (pattern);
571 	return (arr);
572 }
573