better tests
[pspp] / src / data / mdd-writer.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 2018 Free Software Foundation, Inc.
3
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (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
12    GNU General Public License for more details.
13
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <http://www.gnu.org/licenses/>. */
16
17 #include <config.h>
18
19 #include "data/mdd-writer.h"
20
21 #include <errno.h>
22 #include <libxml/xmlwriter.h>
23 #include <stdbool.h>
24 #include <string.h>
25 #include <sys/stat.h>
26 #include <time.h>
27
28 #include "data/dictionary.h"
29 #include "data/file-handle-def.h"
30 #include "data/format.h"
31 #include "data/make-file.h"
32 #include "data/missing-values.h"
33 #include "data/mrset.h"
34 #include "data/short-names.h"
35 #include "data/value-labels.h"
36 #include "data/variable.h"
37 #include "libpspp/message.h"
38 #include "libpspp/misc.h"
39 #include "libpspp/string-map.h"
40 #include "libpspp/string-set.h"
41 #include "libpspp/version.h"
42
43 #include "gl/c-ctype.h"
44 #include "gl/ftoastr.h"
45 #include "gl/xalloc.h"
46 #include "gl/xmemdup0.h"
47
48 #include "gettext.h"
49 #define _(msgid) gettext (msgid)
50 #define N_(msgid) (msgid)
51
52 #define _xml(X) CHAR_CAST (const xmlChar *, X)
53
54 enum val_numeric_type
55   {
56     VAL_INTEGER_TYPE = 1,
57     VAL_STRING_TYPE = 2,
58     VAL_CATEGORICAL_TYPE = 3,
59     VAL_DATETIME_TYPE = 5,
60     VAL_DECIMAL_TYPE = 6
61   };
62
63 /* Get the numeric type of the variable. */
64 static enum val_numeric_type
65 var_get_numeric_type_ (const struct variable *var)
66 {
67   const struct fmt_spec *print = var_get_print_format (var);
68   if (var_get_type (var) == VAL_STRING)
69     return VAL_STRING_TYPE;
70
71   if (var_has_value_labels (var))
72     return VAL_CATEGORICAL_TYPE;
73
74   if (print->d > 0)
75     return VAL_DECIMAL_TYPE;
76
77   if (print->type == FMT_DATETIME)
78     return VAL_DATETIME_TYPE;
79
80   if (print->type == FMT_F)
81     return VAL_INTEGER_TYPE;
82
83   return VAL_CATEGORICAL_TYPE;
84 }
85
86 /* Metadata file writer. */
87 struct mdd_writer
88   {
89     struct file_handle *fh;     /* File handle. */
90     struct fh_lock *lock;       /* Mutual exclusion for file. */
91     FILE *file;                 /* File stream. */
92     struct replace_file *rf;    /* Ticket for replacing output file. */
93
94     xmlTextWriter *writer;
95   };
96
97 /* Either a variable or a multi-response set. */
98 struct var_or_mrset
99   {
100     /* If true, the union contains a multi-response set.  Otherwise, it
101        contains a variable. */
102     bool is_mrset;
103     union
104       {
105         const struct mrset *mrset;
106         const struct variable *variable;
107       };
108   };
109
110 struct all_dict_variables
111   {
112     struct var_or_mrset *vars;
113     size_t count;
114   };
115
116 struct all_dict_variables
117 all_variables (struct dictionary *dict);
118
119 /* Extract all variables in a dictionary, both normal and multi.
120
121    Excludes variables which are subvariables of an MRSET. */
122 struct all_dict_variables
123 all_variables (struct dictionary *dict)
124 {
125   size_t n_vars = dict_get_n_vars (dict);
126
127   /* Start with a set of all variable names. */
128   struct string_set var_names = STRING_SET_INITIALIZER (var_names);
129   for (size_t i = 0; i < n_vars; ++i)
130     {
131       const struct variable *var = dict_get_var (dict, i);
132       string_set_insert (&var_names, var_get_name (var));
133     }
134
135   /* For each MRSET M, remove all subvariable names of M from S. */
136   size_t n_sets = dict_get_n_mrsets (dict);
137   for (size_t set_idx = 0; set_idx < n_sets; set_idx++)
138     {
139       const struct mrset *mrset = dict_get_mrset (dict, set_idx);
140       for (size_t i = 0; i < mrset->n_vars; ++i)
141         {
142           const struct variable *var = mrset->vars[i];
143           string_set_delete (&var_names, var_get_name (var));
144         }
145     }
146
147   /* Add the number of remaining variables to the number of MRSets. */
148   size_t var_count = n_sets + string_set_count (&var_names);
149
150   /* Allocate an array of var_or_mrset pointers (initially null). */
151   struct var_or_mrset *var_or_mrset_array
152     = XCALLOC (var_count, struct var_or_mrset);
153
154   /* Fill the array. */
155   struct string_set added_mrsets = STRING_SET_INITIALIZER (added_mrsets);
156
157   size_t var_idx = 0;
158   for (size_t i = 0; i < n_vars; ++i)
159     {
160       const struct variable *var = dict_get_var (dict, i);
161       bool found_in_mrset = false;
162       for (size_t set_idx = 0; set_idx < n_sets; set_idx++)
163         {
164           const struct mrset *mrset = dict_get_mrset (dict, set_idx);
165           for (size_t i = 0; i < mrset->n_vars; ++i)
166             {
167               const struct variable *subvar = mrset->vars[i];
168               if (!strcmp (var_get_name (var), var_get_name (subvar)))
169                 {
170                   /* Then this variable is a member of this MRSet. */
171                   found_in_mrset = true;
172
173                   /* Check if this MRSet has already been added and add
174                      otherwise. */
175                   if (!string_set_contains (&added_mrsets, mrset->name))
176                     {
177                       string_set_insert (&added_mrsets, mrset->name);
178
179                       assert (var_idx < var_count);
180                       struct var_or_mrset *v_o_m
181                         = &var_or_mrset_array[var_idx++];
182                       v_o_m->is_mrset = true;
183                       v_o_m->mrset = mrset;
184                     }
185                 }
186             }
187         }
188
189       /* If the variable wasn't found to be a member of any MRSets, record it as
190          a normal variable. */
191       if (!found_in_mrset)
192         {
193           /* The variable is not part of a multi-response set. */
194           assert (var_idx < var_count);
195           struct var_or_mrset *v_o_m = &var_or_mrset_array[var_idx++];
196           v_o_m->is_mrset = false;
197           v_o_m->variable = var;
198         }
199     }
200
201   /* Ensure that we filled up the array. */
202   assert (var_idx == var_count);
203
204   /* Cleanup. */
205   string_set_destroy (&added_mrsets);
206   string_set_destroy (&var_names);
207
208   struct all_dict_variables result;
209   result.vars = var_or_mrset_array;
210   result.count = var_count;
211   return result;
212 }
213
214
215 /* Returns true if an I/O error has occurred on WRITER, false otherwise. */
216 static bool
217 mdd_write_error (const struct mdd_writer *writer)
218 {
219   return ferror (writer->file);
220 }
221
222 static bool
223 mdd_close (struct mdd_writer *w)
224 {
225   if (!w)
226     return true;
227
228   if (w->writer)
229     xmlFreeTextWriter (w->writer);
230
231   bool ok = true;
232   if (w->file)
233     {
234       fflush (w->file);
235
236       ok = !mdd_write_error (w);
237       if (fclose (w->file) == EOF)
238         ok = false;
239
240       if (!ok)
241         msg (ME, _("An I/O error occurred writing metadata file `%s'."),
242              fh_get_file_name (w->fh));
243
244       if (ok ? !replace_file_commit (w->rf) : !replace_file_abort (w->rf))
245         ok = false;
246     }
247
248   fh_unlock (w->lock);
249   fh_unref (w->fh);
250
251   free (w);
252
253   return ok;
254 }
255
256 static void
257 write_empty_element (xmlTextWriter *writer, const char *name)
258 {
259   xmlTextWriterStartElement (writer, _xml (name));
260   xmlTextWriterEndElement (writer);
261 }
262
263 static void
264 write_attr (xmlTextWriter *writer, const char *key, const char *value)
265 {
266   xmlTextWriterWriteAttribute (writer, _xml (key), _xml (value));
267 }
268
269 static void
270 write_global_name_space (xmlTextWriter *writer)
271 {
272   write_attr (writer, "global-name-space", "-1");
273 }
274
275 static void
276 write_xml_lang (xmlTextWriter *writer)
277 {
278   /* XXX should write real language */
279   xmlTextWriterWriteAttributeNS (writer, _xml ("xml"), _xml ("lang"), NULL,
280                                  _xml ("en-US"));
281 }
282
283 static void
284 write_value_label_value (xmlTextWriter *writer, const struct val_lab *vl,
285                          int width)
286 {
287   /* XXX below would better use syntax_gen_value(). */
288   const union value *value = val_lab_get_value (vl);
289   if (width)
290     {
291       char *s = xmemdup0 (value->s, width);
292       xmlTextWriterWriteAttribute (writer, _xml ("value"), _xml (s));
293       free (s);
294     }
295   else
296     {
297       char s[DBL_BUFSIZE_BOUND];
298
299       c_dtoastr (s, sizeof s, 0, 0, value->f);
300       xmlTextWriterWriteAttribute (writer, _xml ("value"), _xml (s));
301     }
302 }
303
304 static void
305 write_context (xmlTextWriter *writer, const char *name,
306                const char *alternative)
307 {
308   xmlTextWriterStartElement (writer, _xml ("context"));
309   write_attr (writer, "name", name);
310   if (alternative)
311     {
312       xmlTextWriterStartElement (writer, _xml ("alternatives"));
313       xmlTextWriterStartElement (writer, _xml ("alternative"));
314       write_attr (writer, "name", alternative);
315       xmlTextWriterEndElement (writer);
316       write_empty_element (writer, "deleted");
317       xmlTextWriterEndElement (writer);
318     }
319   xmlTextWriterEndElement (writer);
320 }
321
322 static char *
323 name_to_id (const char *name)
324 {
325   char *id = xmalloc (strlen (name) + 2);
326   char *d = id;
327   for (const char *s = name; *s; s++)
328     {
329       if (c_isalpha (*s))
330         *d++ = c_tolower (*s);
331       else if (c_isdigit (*s))
332         {
333           if (d == id)
334             *d++ = '_';
335           *d++ = *s;
336         }
337       else
338         {
339           if (d == id || d[-1] != '_')
340             *d++ = '_';
341         }
342     }
343   if (d > id && d[-1] == '_')
344     d--;
345   *d = '\0';
346
347   return id;
348 }
349
350 static void
351 write_variable_section (xmlTextWriter *writer, const struct variable *var, int id)
352 {
353   xmlTextWriterStartElement (writer, _xml ("variable"));
354   write_attr (writer, "name", var_get_name (var));
355
356   bool is_string = var_get_type (var) == VAL_STRING;
357
358   int type = var_get_numeric_type_ (var);
359   xmlTextWriterWriteFormatAttribute (writer, _xml ("type"), "%d", type);
360
361   int max = is_string ? var_get_width (var) : 1;
362   xmlTextWriterWriteFormatAttribute (writer, _xml ("max"), "%d", max);
363
364   write_attr (writer, "maxtype", "3");
365
366   const char *label = var_get_label (var);
367   if (label)
368     {
369       xmlTextWriterStartElement (writer, _xml ("labels"));
370       write_attr (writer, "context", "LABEL");
371
372       xmlTextWriterStartElement (writer, _xml ("text"));
373       write_attr (writer, "context", "ANALYSIS");
374       write_xml_lang (writer);
375       xmlTextWriterWriteString (writer, _xml (label));
376       xmlTextWriterEndElement (writer);
377
378       xmlTextWriterEndElement (writer);
379     }
380
381   const struct val_labs *val_labs = var_get_value_labels (var);
382   size_t n_vls = val_labs_count (val_labs);
383   if (n_vls)
384     {
385       const struct val_lab **vls = val_labs_sorted (val_labs);
386
387       /* <categories/> */
388       xmlTextWriterStartElement (writer, _xml ("categories"));
389       write_global_name_space (writer);
390       int width = var_get_width (var);
391       for (size_t j = 0; j < n_vls; j++)
392         {
393           const struct val_lab *vl = vls[j];
394           const union value *value = val_lab_get_value (vl);
395
396           /* <category> */
397           xmlTextWriterStartElement (writer, _xml ("category"));
398           xmlTextWriterWriteFormatAttribute (writer, _xml ("id"), "_%d", id);
399
400           char *name = name_to_id (val_lab_get_label (vl));
401           write_attr (writer, "name", name);
402           free (name);
403
404           /* If the value here is missing, annotate it.
405
406              XXX only checking "user" here because not sure of correct other
407              cases. */
408           if (var_is_value_missing (var, value, MV_USER))
409             write_attr (writer, "missing", "user");
410           else if (var_is_value_missing (var, value, MV_SYSTEM))
411             write_attr (writer, "missing", "system");
412
413           /* <properties/> */
414           xmlTextWriterStartElement (writer, _xml ("properties"));
415           xmlTextWriterStartElement (writer, _xml ("property"));
416           write_attr (writer, "name", "Value");
417           write_value_label_value (writer, vl, width);
418           write_attr (writer, "type", "5");
419           write_attr (writer, "context", "Analysis");
420           xmlTextWriterEndElement (writer); /* </property> */
421           xmlTextWriterEndElement (writer); /* </properties> */
422
423           /* <labels/> */
424           xmlTextWriterStartElement (writer, _xml ("labels"));
425           write_attr (writer, "context", "LABEL");
426           xmlTextWriterStartElement (writer, _xml ("text"));
427           write_attr (writer, "context", "ANALYSIS");
428           write_xml_lang (writer);
429           xmlTextWriterWriteString (writer,
430                                     _xml (val_lab_get_label (vl)));
431           xmlTextWriterEndElement (writer); /* </text> */
432           xmlTextWriterEndElement (writer); /* </labels> */
433
434
435           /* </category> */
436           xmlTextWriterEndElement (writer);
437         }
438       write_empty_element (writer, "deleted");
439       xmlTextWriterEndElement (writer); /* </categories> */
440
441       free (vls);
442     }
443   /* </variable> */
444   xmlTextWriterEndElement (writer);
445 }
446
447 bool
448 mdd_write (struct file_handle *fh, struct dictionary *dict,
449            const char *sav_name)
450 {
451   struct mdd_writer *w = XZALLOC (struct mdd_writer);
452   size_t n_vars = dict_get_n_vars (dict);
453
454   /* Open file handle as an exclusive writer. */
455   /* TRANSLATORS: this fragment will be interpolated into
456      messages in fh_lock() that identify types of files. */
457   w->lock = fh_lock (fh, FH_REF_FILE, N_("metadata file"), FH_ACC_WRITE, true);
458   if (w->lock == NULL)
459     goto error;
460
461   /* Create the file on disk. */
462   w->rf = replace_file_start (fh, "wb", 0444, &w->file);
463   if (w->rf == NULL)
464     {
465       msg (ME, _("Error opening `%s' for writing as a metadata file: %s."),
466            fh_get_file_name (fh), strerror (errno));
467       goto error;
468     }
469
470   w->writer = xmlNewTextWriter (xmlOutputBufferCreateFile (w->file, NULL));
471   if (!w->writer)
472     {
473       msg (ME, _("Internal error creating xmlTextWriter.  "
474                  "Please report this to %s."), PACKAGE_BUGREPORT);
475       goto error;
476     }
477
478   xmlTextWriterStartDocument (w->writer, NULL, "UTF-8", NULL);
479
480   /* The MDD file contents seem to roughly correspond to the object model
481      documentatation at:
482      https://support.unicomsi.com/manuals/intelligence/75/DDL/MDM/docjet/metadata/chm/contentsf.html.  */
483
484   /* <?xml-stylesheet type="text/xsl" href="mdd.xslt"?> */
485   xmlTextWriterStartPI (w->writer, _xml ("xml-stylesheet"));
486   xmlTextWriterWriteString (w->writer,
487                             _xml ("type=\"text/xsl\" href=\"mdd.xslt\""));
488   xmlTextWriterEndPI (w->writer);
489
490   xmlTextWriterStartElement (w->writer, _xml ("xml"));
491
492   /* <mdm:metadata xmlns:mdm="http://www.spss.com/mr/dm/metadatamodel/Arc
493      3/2000-02-04" mdm_createversion="7.0.0.0.331"
494      mdm_lastversion="7.0.0.0.331" id="c4c181c1-0d7c-42e3-abcd-f08296d1dfdc"
495      data_version="9" data_sub_version="1" systemvariable="0"
496      dbfiltervalidation="-1"> */
497   xmlTextWriterStartElementNS (
498     w->writer, _xml ("mdm"), _xml ("metadata"),
499     _xml ("http://www.spss.com/mr/dm/metadatamodel/Arc%203/2000-02-04"));
500   static const struct pair
501     {
502       const char *key, *value;
503     }
504   pairs[] =
505     {
506       { "mdm_createversion", "7.0.0.0.331" },
507       { "mdm_lastversion", "7.0.0.0.331" },
508       { "id", "c4c181c1-0d7c-42e3-abcd-f08296d1dfdc" },
509       { "data_version", "9" },
510       { "data_sub_version", "1" },
511       { "systemvariable", "0" },
512       { "dbfiltervalidation", "-1" },
513     };
514   const int n_pairs = sizeof pairs / sizeof *pairs;
515   for (const struct pair *p = pairs; p < &pairs[n_pairs]; p++)
516     xmlTextWriterWriteAttribute (w->writer, _xml (p->key), _xml (p->value));
517
518   /* <atoms/> */
519   xmlTextWriterStartElement (w->writer, _xml ("atoms"));
520   /* XXX Real files contain a list of languages and a few other random strings
521      here in <atom name="..."/> elements.  It's really not clear what they're
522      good for. */
523   xmlTextWriterEndElement (w->writer);
524
525   /* <datasources> */
526   xmlTextWriterStartElement (w->writer, _xml ("datasources"));
527   xmlTextWriterWriteAttribute (w->writer, _xml ("default"), _xml ("mrSavDsc"));
528
529   /* <connection/> */
530   xmlTextWriterStartElement (w->writer, _xml ("connection"));
531   write_attr (w->writer, "name", "mrSavDsc");
532   write_attr (w->writer, "dblocation", sav_name);
533   write_attr (w->writer, "cdscname", "mrSavDsc");
534   write_attr (w->writer, "project", "126");
535
536   struct all_dict_variables allvars = all_variables (dict);
537   struct var_or_mrset *var_or_mrset_array = allvars.vars;
538   size_t var_count = allvars.count;
539
540   short_names_assign (dict);
541   for (size_t i = 0; i < var_count; i++)
542     {
543       const struct var_or_mrset var_or_mrset = var_or_mrset_array[i];
544       xmlTextWriterStartElement (w->writer, _xml ("var"));
545       if (var_or_mrset.is_mrset)
546         {
547           const struct mrset *mrset = var_or_mrset.mrset;
548           write_attr (w->writer, "fullname", mrset->name + 1);
549           write_attr (w->writer, "aliasname", mrset->name);
550
551           for (size_t subvar_idx = 0; subvar_idx < mrset->n_vars; subvar_idx++)
552             {
553               xmlTextWriterStartElement(w->writer, _xml ("subalias"));
554               xmlTextWriterWriteFormatAttribute (w->writer, _xml ("index"),
555                                                  "%zu", subvar_idx);
556               write_attr (w->writer, "name",
557                           var_get_name (mrset->vars[subvar_idx]));
558
559               xmlTextWriterEndElement (w->writer); /* subalias */
560             }
561         }
562       else
563         {
564           const struct variable *var = var_or_mrset.variable;
565
566           char *short_name = xstrdup (var_get_short_name (var, 0));
567           for (char *p = short_name; *p; p++)
568             *p = c_tolower (*p);
569           write_attr (w->writer, "fullname", short_name);
570           free (short_name);
571
572           write_attr (w->writer, "aliasname", var_get_name (var));
573
574           const struct val_labs *val_labs = var_get_value_labels (var);
575           size_t n_vls = val_labs_count (val_labs);
576           if (n_vls)
577             {
578               const struct val_lab **vls = val_labs_sorted (val_labs);
579
580               xmlTextWriterStartElement (w->writer, _xml ("nativevalues"));
581               int width = var_get_width (var);
582               for (size_t j = 0; j < n_vls; j++)
583                 {
584                   const struct val_lab *vl = vls[j];
585                   xmlTextWriterStartElement (w->writer, _xml ("nativevalue"));
586
587                   char *fullname = name_to_id (val_lab_get_label (vl));
588                   write_attr (w->writer, "fullname", fullname);
589                   free (fullname);
590
591                   write_value_label_value (w->writer, vl, width);
592                   xmlTextWriterEndElement (w->writer);
593                 }
594               xmlTextWriterEndElement (w->writer);
595
596               free (vls);
597             }
598
599         }
600       xmlTextWriterEndElement (w->writer); /* var */
601     }
602
603   xmlTextWriterEndElement (w->writer); /* connection */
604   xmlTextWriterEndElement (w->writer); /* datasources */
605
606   /* If the dictionary has a label, record it here */
607   const char *file_label = dict_get_label (dict);
608   if (file_label != NULL)
609     {
610       xmlTextWriterStartElement (w->writer, _xml ("labels"));
611       write_attr (w->writer, "context", "LABEL");
612       xmlTextWriterStartElement (w->writer, _xml ("text"));
613
614       write_attr (w->writer, "context", "ANALYSIS");
615       write_xml_lang (w->writer);
616       xmlTextWriterWriteString (w->writer, _xml (file_label));
617
618       /* </text> */
619       xmlTextWriterEndElement (w->writer);
620
621       /* </labels> */
622       xmlTextWriterEndElement (w->writer);
623     }
624
625   /* We reserve ids 1...N_VARS for variables and then start other ids after
626      that. */
627   int id = dict_get_n_vars (dict) + 1;
628
629   /* <definition/> */
630   xmlTextWriterStartElement (w->writer, _xml ("definition"));
631   for (size_t i = 0; i < var_count; i++)
632     {
633       xmlTextWriterWriteFormatAttribute (w->writer, _xml ("id"), "%zu", i + 1);
634       const struct var_or_mrset var_or_mrset = var_or_mrset_array[i];
635
636       if (var_or_mrset.is_mrset)
637         {
638           const struct mrset *mrset = var_or_mrset.mrset;
639           xmlTextWriterStartElement (w->writer, _xml ("variable"));
640           write_attr (w->writer, "name", mrset->name);
641
642           /* Use the type of the first subvariable as the type of the MRSET? */
643           write_attr (w->writer, "type", "3");
644
645           xmlTextWriterStartElement (w->writer, _xml ("properties"));
646           xmlTextWriterStartElement (w->writer, _xml ("property"));
647           write_attr (w->writer, "name", "QvLabel");
648           write_attr (w->writer, "value", mrset->name);
649           write_attr (w->writer, "type", "8");
650           write_attr (w->writer, "context", "Analysis");
651           /* </property> */
652           xmlTextWriterEndElement (w->writer);
653           /* </properties> */
654           xmlTextWriterEndElement (w->writer);
655
656
657           xmlTextWriterStartElement (w->writer, _xml ("labels"));
658           write_attr (w->writer, "context", "LABEL");
659           xmlTextWriterStartElement (w->writer, _xml ("text"));
660
661           write_attr (w->writer, "context", "ANALYSIS");
662           write_xml_lang (w->writer);
663           xmlTextWriterWriteString (w->writer, _xml (mrset->label));
664
665           /* </text> */
666           xmlTextWriterEndElement (w->writer);
667
668           /* </labels> */
669           xmlTextWriterEndElement (w->writer);
670
671           xmlTextWriterStartElement (w->writer, _xml ("categories"));
672           write_attr (w->writer, "global-name-space", "-1");
673           write_empty_element (w->writer, "deleted");
674
675           /* Individual categories */
676           int value = 2;
677           for (size_t var_idx = 0; var_idx < mrset->n_vars; ++var_idx)
678             {
679               const struct variable *subvar = mrset->vars[var_idx];
680               value += 2;
681               xmlTextWriterStartElement (w->writer, _xml ("category"));
682               write_attr (w->writer, "context", "LABEL");
683               char *name_without_spaces = strdup (var_get_name (subvar));
684               for (size_t i = 0; name_without_spaces[i]; ++i)
685                 if (name_without_spaces[i] == ' ') name_without_spaces[i] = '_';
686               write_attr (w->writer, "name", name_without_spaces);
687               free (name_without_spaces);
688
689               xmlTextWriterStartElement (w->writer, _xml ("properties"));
690               xmlTextWriterStartElement (w->writer, _xml ("property"));
691               write_attr (w->writer, "name", "QvBasicNum");
692               xmlTextWriterWriteFormatAttribute
693                 (w->writer, _xml ("value"), "%d", value);
694               write_attr (w->writer, "type", "3");
695               write_attr (w->writer, "context", "Analysis");
696               /* </property> */
697               xmlTextWriterEndElement (w->writer);
698               /* </properties> */
699               xmlTextWriterEndElement (w->writer);
700
701               xmlTextWriterStartElement (w->writer, _xml ("labels"));
702               write_attr (w->writer, "context", "LABEL");
703
704               xmlTextWriterStartElement (w->writer, _xml ("text"));
705               write_attr (w->writer, "context", "ANALYSIS");
706               write_xml_lang (w->writer);
707               xmlTextWriterWriteString (w->writer, _xml (var_get_label (subvar)));
708               /* </text> */
709               xmlTextWriterEndElement (w->writer);
710
711
712               /* </labels> */
713               xmlTextWriterEndElement (w->writer);
714               /* </category> */
715               xmlTextWriterEndElement (w->writer);
716             }
717
718           /* </categories> */
719           xmlTextWriterEndElement (w->writer);
720           /* </variable> */
721           xmlTextWriterEndElement (w->writer);
722         }
723       else
724         {
725           const struct variable *var = var_or_mrset.variable;
726           write_variable_section(w->writer, var, id++);
727         }
728     }
729   xmlTextWriterEndElement (w->writer); /* </definition> */
730
731   write_empty_element (w->writer, "system");
732   write_empty_element (w->writer, "systemrouting");
733   write_empty_element (w->writer, "mappings");
734
735   /* <design/> */
736   xmlTextWriterStartElement (w->writer, _xml ("design"));
737   xmlTextWriterStartElement (w->writer, _xml ("fields"));
738   write_attr (w->writer, "name", "@fields");
739   write_global_name_space (w->writer);
740   for (size_t i = 0; i < n_vars; i++)
741     {
742       const struct variable *var = dict_get_var (dict, i);
743       xmlTextWriterStartElement (w->writer, _xml ("variable"));
744       xmlTextWriterWriteFormatAttribute (w->writer, _xml ("id"),
745                                          "_%zu", i + 1);
746       write_attr (w->writer, "name", var_get_name (var));
747       xmlTextWriterWriteFormatAttribute (w->writer, _xml ("ref"),
748                                          "%zu", i + 1);
749       xmlTextWriterEndElement (w->writer);
750     }
751   write_empty_element (w->writer, "deleted");
752   xmlTextWriterEndElement (w->writer);
753   xmlTextWriterStartElement (w->writer, _xml ("types"));
754   write_attr (w->writer, "name", "@types");
755   write_global_name_space (w->writer);
756   write_empty_element (w->writer, "deleted");
757   xmlTextWriterEndElement (w->writer);
758   xmlTextWriterStartElement (w->writer, _xml ("pages"));
759   write_attr (w->writer, "name", "@pages");
760   write_global_name_space (w->writer);
761   write_empty_element (w->writer, "deleted");
762   xmlTextWriterEndElement (w->writer);
763   xmlTextWriterStartElement (w->writer, _xml ("routings"));
764   xmlTextWriterStartElement (w->writer, _xml ("scripts"));
765   write_empty_element (w->writer, "deleted");
766   xmlTextWriterEndElement (w->writer);
767   xmlTextWriterEndElement (w->writer);
768   xmlTextWriterEndElement (w->writer);
769
770   /* <languages/> */
771   /* XXX should use the real language */
772   xmlTextWriterStartElement (w->writer, _xml ("languages"));
773   write_attr (w->writer, "base", "EN-US");
774   xmlTextWriterStartElement (w->writer, _xml ("language"));
775   write_attr (w->writer, "name", "EN-US");
776   write_attr (w->writer, "id", "0409");
777   xmlTextWriterEndElement (w->writer);
778   write_empty_element (w->writer, "deleted");
779   xmlTextWriterEndElement (w->writer);
780
781   /* <contexts/> */
782   xmlTextWriterStartElement (w->writer, _xml ("contexts"));
783   write_attr (w->writer, "base", "Analysis");
784   write_context (w->writer, "ANALYSIS", "QUESTION");
785   write_context (w->writer, "QUESTION", "ANALYSIS");
786   write_context (w->writer, "WEBAPP", NULL);
787   write_empty_element (w->writer, "deleted");
788   xmlTextWriterEndElement (w->writer);
789
790   /* <labeltypes/> */
791   xmlTextWriterStartElement (w->writer, _xml ("labeltypes"));
792   write_attr (w->writer, "base", "label");
793   write_context (w->writer, "LABEL", NULL);
794   write_empty_element (w->writer, "deleted");
795   xmlTextWriterEndElement (w->writer);
796
797   /* <routingcontexts/> */
798   write_empty_element (w->writer, "routingcontexts");
799
800   /* <scripttypes/> */
801   xmlTextWriterStartElement (w->writer, _xml ("scripttypes"));
802   write_attr (w->writer, "base", "mrScriptBasic");
803   write_context (w->writer, "MRSCRIPTBASIC", NULL);
804   write_empty_element (w->writer, "deleted");
805   xmlTextWriterEndElement (w->writer);
806
807   /* <versionlist/> */
808   write_empty_element (w->writer, "versionlist");
809
810   /* <categorymap/> */
811   xmlTextWriterStartElement (w->writer, _xml ("categorymap"));
812   struct string_set categories = STRING_SET_INITIALIZER (categories);
813   for (size_t i = 0; i < n_vars; i++)
814     {
815       const struct variable *var = dict_get_var (dict, i);
816       const struct val_labs *val_labs = var_get_value_labels (var);
817       size_t n_vls = val_labs_count (val_labs);
818       if (n_vls)
819         {
820           const struct val_lab **vls = val_labs_sorted (val_labs);
821
822           for (size_t j = 0; j < n_vls; j++)
823             {
824               const struct val_lab *vl = vls[j];
825
826               char *label = name_to_id (val_lab_get_label (vl));
827               if (string_set_insert_nocopy (&categories, label))
828                 {
829                   xmlTextWriterStartElement (w->writer, _xml ("categoryid"));
830                   write_attr (w->writer, "name", label);
831                   xmlTextWriterWriteFormatAttribute (
832                     w->writer, _xml ("value"),
833                     "%zu", string_set_count (&categories));
834                   xmlTextWriterEndElement (w->writer);
835                 }
836             }
837
838           free (vls);
839         }
840     }
841   string_set_destroy (&categories);
842   xmlTextWriterEndElement (w->writer);
843
844   /* <savelogs/> */
845   xmlTextWriterStartElement (w->writer, _xml ("savelogs"));
846   xmlTextWriterStartElement (w->writer, _xml ("savelog"));
847   write_attr (w->writer, "fileversion", "7.0.0.0.331");
848   write_attr (w->writer, "versionset", "");
849   write_attr (w->writer, "username", "Administrator");
850   time_t t;
851   if (time (&t) == (time_t) -1)
852     write_attr (w->writer, "date", "01/01/1970 00:00:00 AM");
853   else
854     {
855       struct tm *tm = localtime (&t);
856       int hour = tm->tm_hour % 12;
857       xmlTextWriterWriteFormatAttribute (w->writer, _xml ("date"),
858                                          "%02d/%02d/%04d %02d:%02d:%02d %s",
859                                          tm->tm_mon + 1, tm->tm_mday,
860                                          tm->tm_year + 1900,
861                                          hour ? hour : 12, tm->tm_min,
862                                          tm->tm_sec,
863                                          tm->tm_hour < 12 ? "AM" : "PM");
864     }
865   xmlTextWriterStartElement (w->writer, _xml ("user"));
866   write_attr (w->writer, "name", "pspp");
867   write_attr (w->writer, "fileversion", version);
868   write_attr (w->writer, "comment", "Written by GNU PSPP");
869   xmlTextWriterEndElement (w->writer);
870   xmlTextWriterEndElement (w->writer);
871   xmlTextWriterEndElement (w->writer);
872
873   /* </xml> */
874   xmlTextWriterEndElement (w->writer);
875
876   xmlTextWriterEndDocument (w->writer);
877
878   free(var_or_mrset_array);
879
880 error:
881   mdd_close (w);
882   return NULL;
883 }