bc8b54cd8b786aa74f3454854be9a3b004a2e0cb
[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/make-file.h"
31 #include "data/short-names.h"
32 #include "data/value-labels.h"
33 #include "data/variable.h"
34 #include "libpspp/message.h"
35 #include "libpspp/misc.h"
36 #include "libpspp/string-set.h"
37 #include "libpspp/version.h"
38
39 #include "gl/c-ctype.h"
40 #include "gl/ftoastr.h"
41 #include "gl/xalloc.h"
42 #include "gl/xmemdup0.h"
43
44 #include "gettext.h"
45 #define _(msgid) gettext (msgid)
46 #define N_(msgid) (msgid)
47
48 #define _xml(X) CHAR_CAST (const xmlChar *, X)
49
50 /* Metadata file writer. */
51 struct mdd_writer
52   {
53     struct file_handle *fh;     /* File handle. */
54     struct fh_lock *lock;       /* Mutual exclusion for file. */
55     FILE *file;                 /* File stream. */
56     struct replace_file *rf;    /* Ticket for replacing output file. */
57
58     xmlTextWriter *writer;
59   };
60
61 /* Returns true if an I/O error has occurred on WRITER, false otherwise. */
62 static bool
63 mdd_write_error (const struct mdd_writer *writer)
64 {
65   return ferror (writer->file);
66 }
67
68 static bool
69 mdd_close (struct mdd_writer *w)
70 {
71   if (!w)
72     return true;
73
74   if (w->writer)
75     xmlFreeTextWriter (w->writer);
76
77   bool ok = true;
78   if (w->file)
79     {
80       fflush (w->file);
81
82       ok = !mdd_write_error (w);
83       if (fclose (w->file) == EOF)
84         ok = false;
85
86       if (!ok)
87         msg (ME, _("An I/O error occurred writing metadata file `%s'."),
88              fh_get_file_name (w->fh));
89
90       if (ok ? !replace_file_commit (w->rf) : !replace_file_abort (w->rf))
91         ok = false;
92     }
93
94   fh_unlock (w->lock);
95   fh_unref (w->fh);
96
97   free (w);
98
99   return ok;
100 }
101
102 static void
103 write_empty_element (xmlTextWriter *writer, const char *name)
104 {
105   xmlTextWriterStartElement (writer, _xml (name));
106   xmlTextWriterEndElement (writer);
107 }
108
109 static void
110 write_attr (xmlTextWriter *writer, const char *key, const char *value)
111 {
112   xmlTextWriterWriteAttribute (writer, _xml (key), _xml (value));
113 }
114
115 static void
116 write_global_name_space (xmlTextWriter *writer)
117 {
118   write_attr (writer, "global-name-space", "-1");
119 }
120
121 static void
122 write_xml_lang (xmlTextWriter *writer)
123 {
124   /* XXX should write real language */
125   xmlTextWriterWriteAttributeNS (writer, _xml ("xml"), _xml ("lang"), NULL,
126                                  _xml ("en-US"));
127 }
128
129 static void
130 write_value_label_value (xmlTextWriter *writer, const struct val_lab *vl,
131                          int width)
132 {
133   /* XXX below would better use syntax_gen_value(). */
134   const union value *value = val_lab_get_value (vl);
135   if (width)
136     {
137       char *s = xmemdup0 (value_str (value, width), width);
138       xmlTextWriterWriteAttribute (writer, _xml ("value"), _xml (s));
139       free (s);
140     }
141   else
142     {
143       char s[DBL_BUFSIZE_BOUND];
144
145       c_dtoastr (s, sizeof s, 0, 0, value->f);
146       xmlTextWriterWriteAttribute (writer, _xml ("value"), _xml (s));
147     }
148 }
149
150 static void
151 write_context (xmlTextWriter *writer, const char *name,
152                const char *alternative)
153 {
154   xmlTextWriterStartElement (writer, _xml ("context"));
155   write_attr (writer, "name", name);
156   if (alternative)
157     {
158       xmlTextWriterStartElement (writer, _xml ("alternatives"));
159       xmlTextWriterStartElement (writer, _xml ("alternative"));
160       write_attr (writer, "name", alternative);
161       xmlTextWriterEndElement (writer);
162       write_empty_element (writer, "deleted");
163       xmlTextWriterEndElement (writer);
164     }
165   xmlTextWriterEndElement (writer);
166 }
167
168 static char *
169 name_to_id (const char *name)
170 {
171   char *id = xmalloc (strlen (name) + 2);
172   char *d = id;
173   for (const char *s = name; *s; s++)
174     {
175       if (c_isalpha (*s))
176         *d++ = c_tolower (*s);
177       else if (c_isdigit (*s))
178         {
179           if (d == id)
180             *d++ = '_';
181           *d++ = *s;
182         }
183       else
184         {
185           if (d == id || d[-1] != '_')
186             *d++ = '_';
187         }
188     }
189   if (d > id && d[-1] == '_')
190     d--;
191   *d = '\0';
192
193   return id;
194 }
195
196 bool
197 mdd_write (struct file_handle *fh, struct dictionary *dict,
198            const char *sav_name)
199 {
200   struct mdd_writer *w = xzalloc (sizeof *w);
201
202   /* Open file handle as an exclusive writer. */
203   /* TRANSLATORS: this fragment will be interpolated into
204      messages in fh_lock() that identify types of files. */
205   w->lock = fh_lock (fh, FH_REF_FILE, N_("metadata file"), FH_ACC_WRITE, true);
206   if (w->lock == NULL)
207     goto error;
208
209   /* Create the file on disk. */
210   w->rf = replace_file_start (fh, "wb", 0444, &w->file);
211   if (w->rf == NULL)
212     {
213       msg (ME, _("Error opening `%s' for writing as a metadata file: %s."),
214            fh_get_file_name (fh), strerror (errno));
215       goto error;
216     }
217
218   w->writer = xmlNewTextWriter (xmlOutputBufferCreateFile (w->file, NULL));
219   if (!w->writer)
220     {
221       msg (ME, _("Internal error creating xmlTextWriter."));
222       goto error;
223     }
224
225   xmlTextWriterStartDocument (w->writer, NULL, "UTF-8", NULL);
226
227   /* The MDD file contents seem to roughly correspond to the object model
228      documentatation at:
229      https://support.unicomsi.com/manuals/intelligence/75/DDL/MDM/docjet/metadata/chm/contentsf.html.  */
230
231   /* <?xml-stylesheet type="text/xsl" href="mdd.xslt"?> */
232   xmlTextWriterStartPI (w->writer, _xml ("xml-stylesheet"));
233   xmlTextWriterWriteString (w->writer,
234                             _xml ("type=\"text/xsl\" href=\"mdd.xslt\""));
235   xmlTextWriterEndPI (w->writer);
236
237   xmlTextWriterStartElement (w->writer, _xml ("xml"));
238
239   /* <mdm:metadata xmlns:mdm="http://www.spss.com/mr/dm/metadatamodel/Arc
240      3/2000-02-04" mdm_createversion="7.0.0.0.331"
241      mdm_lastversion="7.0.0.0.331" id="c4c181c1-0d7c-42e3-abcd-f08296d1dfdc"
242      data_version="9" data_sub_version="1" systemvariable="0"
243      dbfiltervalidation="-1"> */
244   xmlTextWriterStartElementNS (
245     w->writer, _xml ("mdm"), _xml ("metadata"),
246     _xml ("http://www.spss.com/mr/dm/metadatamodel/Arc%203/2000-02-04"));
247   static const struct pair
248     {
249       const char *key, *value;
250     }
251   pairs[] =
252     {
253       { "mdm_createversion", "7.0.0.0.331" },
254       { "mdm_lastversion", "7.0.0.0.331" },
255       { "id", "c4c181c1-0d7c-42e3-abcd-f08296d1dfdc" },
256       { "data_version", "9" },
257       { "data_sub_version", "1" },
258       { "systemvariable", "0" },
259       { "dbfiltervalidation", "-1" },
260     };
261   const int n_pairs = sizeof pairs / sizeof *pairs;
262   for (const struct pair *p = pairs; p < &pairs[n_pairs]; p++)
263     xmlTextWriterWriteAttribute (w->writer, _xml (p->key), _xml (p->value));
264
265   /* <atoms/> */
266   xmlTextWriterStartElement (w->writer, _xml ("atoms"));
267   /* XXX Real files contain a list of languages and a few other random strings
268      here in <atom name="..."/> elements.  It's really not clear what they're
269      good for. */
270   xmlTextWriterEndElement (w->writer);
271
272   /* <datasources> */
273   xmlTextWriterStartElement (w->writer, _xml ("datasources"));
274   xmlTextWriterWriteAttribute (w->writer, _xml ("default"), _xml ("mrSavDsc"));
275
276   /* <connection/> */
277   xmlTextWriterStartElement (w->writer, _xml ("connection"));
278   write_attr (w->writer, "name", "mrSavDsc");
279   write_attr (w->writer, "dblocation", sav_name);
280   write_attr (w->writer, "cdscname", "mrSavDsc");
281   write_attr (w->writer, "project", "126");
282
283   size_t n_vars = dict_get_var_cnt (dict);
284   short_names_assign (dict);
285   for (size_t i = 0; i < n_vars; i++)
286     {
287       const struct variable *var = dict_get_var (dict, i);
288       xmlTextWriterStartElement (w->writer, _xml ("var"));
289
290       char *short_name = xstrdup (var_get_short_name (var, 0));
291       for (char *p = short_name; *p; p++)
292         *p = c_tolower (*p);
293       write_attr (w->writer, "fullname", short_name);
294       free (short_name);
295
296       write_attr (w->writer, "aliasname", var_get_name (var));
297
298       const struct val_labs *val_labs = var_get_value_labels (var);
299       size_t n_vls = val_labs_count (val_labs);
300       if (n_vls)
301         {
302           const struct val_lab **vls = val_labs_sorted (val_labs);
303
304           xmlTextWriterStartElement (w->writer, _xml ("nativevalues"));
305           int width = var_get_width (var);
306           for (size_t j = 0; j < n_vls; j++)
307             {
308               const struct val_lab *vl = vls[j];
309               xmlTextWriterStartElement (w->writer, _xml ("nativevalue"));
310
311               char *fullname = name_to_id (val_lab_get_label (vl));
312               write_attr (w->writer, "fullname", fullname);
313               free (fullname);
314
315               write_value_label_value (w->writer, vl, width);
316               xmlTextWriterEndElement (w->writer);
317             }
318           xmlTextWriterEndElement (w->writer);
319
320           free (vls);
321         }
322
323       xmlTextWriterEndElement (w->writer);
324     }
325   xmlTextWriterEndElement (w->writer);
326   xmlTextWriterEndElement (w->writer);
327
328   /* We reserve ids 1...N_VARS for variables and then start other ids after
329      that. */
330   int id = dict_get_var_cnt (dict) + 1;
331
332   /* <definition/> */
333   xmlTextWriterStartElement (w->writer, _xml ("definition"));
334   for (size_t i = 0; i < n_vars; i++)
335     {
336       const struct variable *var = dict_get_var (dict, i);
337       xmlTextWriterStartElement (w->writer, _xml ("variable"));
338       xmlTextWriterWriteFormatAttribute (w->writer, _xml ("id"), "%zu", i + 1);
339       write_attr (w->writer, "name", var_get_name (var));
340
341       bool is_string = var_get_type (var) == VAL_STRING;
342       int type = is_string ? 2 : 3;
343       xmlTextWriterWriteFormatAttribute (w->writer, _xml ("type"), "%d", type);
344
345       int max = is_string ? var_get_width (var) : 1;
346       xmlTextWriterWriteFormatAttribute (w->writer, _xml ("max"), "%d", max);
347
348       write_attr (w->writer, "maxtype", "3");
349
350       const char *label = var_get_label (var);
351       if (label)
352         {
353           xmlTextWriterStartElement (w->writer, _xml ("labels"));
354           write_attr (w->writer, "context", "LABEL");
355
356           xmlTextWriterStartElement (w->writer, _xml ("text"));
357           write_attr (w->writer, "context", "ANALYSIS");
358           write_xml_lang (w->writer);
359           xmlTextWriterWriteString (w->writer, _xml (label));
360           xmlTextWriterEndElement (w->writer);
361
362           xmlTextWriterEndElement (w->writer);
363         }
364
365       const struct val_labs *val_labs = var_get_value_labels (var);
366       size_t n_vls = val_labs_count (val_labs);
367       if (n_vls)
368         {
369           const struct val_lab **vls = val_labs_sorted (val_labs);
370
371           /* <categories/> */
372           xmlTextWriterStartElement (w->writer, _xml ("categories"));
373           write_global_name_space (w->writer);
374           int width = var_get_width (var);
375           for (size_t j = 0; j < n_vls; j++)
376             {
377               const struct val_lab *vl = vls[j];
378
379               /* <category> */
380               xmlTextWriterStartElement (w->writer, _xml ("category"));
381               xmlTextWriterWriteFormatAttribute (w->writer, _xml ("id"),
382                                                  "_%d", id++);
383
384               char *name = name_to_id (val_lab_get_label (vl));
385               write_attr (w->writer, "name", name);
386               free (name);
387
388               /* <properties/> */
389               xmlTextWriterStartElement (w->writer, _xml ("properties"));
390               xmlTextWriterStartElement (w->writer, _xml ("property"));
391               write_attr (w->writer, "name", "Value");
392               write_value_label_value (w->writer, vl, width);
393               write_attr (w->writer, "type", "5");
394               write_attr (w->writer, "context", "Analysis");
395               xmlTextWriterEndElement (w->writer);
396               xmlTextWriterEndElement (w->writer);
397
398               /* <labels/> */
399               xmlTextWriterStartElement (w->writer, _xml ("labels"));
400               write_attr (w->writer, "context", "LABEL");
401               xmlTextWriterStartElement (w->writer, _xml ("text"));
402               write_attr (w->writer, "context", "ANALYSIS");
403               write_xml_lang (w->writer);
404               xmlTextWriterWriteString (w->writer,
405                                         _xml (val_lab_get_label (vl)));
406               xmlTextWriterEndElement (w->writer);
407               xmlTextWriterEndElement (w->writer);
408
409
410               /* </category> */
411               xmlTextWriterEndElement (w->writer);
412             }
413           write_empty_element (w->writer, "deleted");
414           xmlTextWriterEndElement (w->writer);
415
416           free (vls);
417         }
418
419       xmlTextWriterEndElement (w->writer);
420     }
421   xmlTextWriterEndElement (w->writer);
422
423   write_empty_element (w->writer, "system");
424   write_empty_element (w->writer, "systemrouting");
425   write_empty_element (w->writer, "mappings");
426
427   /* <design/> */
428   xmlTextWriterStartElement (w->writer, _xml ("design"));
429   xmlTextWriterStartElement (w->writer, _xml ("fields"));
430   write_attr (w->writer, "name", "@fields");
431   write_global_name_space (w->writer);
432   for (size_t i = 0; i < n_vars; i++)
433     {
434       const struct variable *var = dict_get_var (dict, i);
435       xmlTextWriterStartElement (w->writer, _xml ("variable"));
436       xmlTextWriterWriteFormatAttribute (w->writer, _xml ("id"),
437                                          "_%zu", i + 1);
438       write_attr (w->writer, "name", var_get_name (var));
439       xmlTextWriterWriteFormatAttribute (w->writer, _xml ("ref"),
440                                          "%zu", i + 1);
441       xmlTextWriterEndElement (w->writer);
442     }
443   write_empty_element (w->writer, "deleted");
444   xmlTextWriterEndElement (w->writer);
445   xmlTextWriterStartElement (w->writer, _xml ("types"));
446   write_attr (w->writer, "name", "@types");
447   write_global_name_space (w->writer);
448   write_empty_element (w->writer, "deleted");
449   xmlTextWriterEndElement (w->writer);
450   xmlTextWriterStartElement (w->writer, _xml ("pages"));
451   write_attr (w->writer, "name", "@pages");
452   write_global_name_space (w->writer);
453   write_empty_element (w->writer, "deleted");
454   xmlTextWriterEndElement (w->writer);
455   xmlTextWriterStartElement (w->writer, _xml ("routings"));
456   xmlTextWriterStartElement (w->writer, _xml ("scripts"));
457   write_empty_element (w->writer, "deleted");
458   xmlTextWriterEndElement (w->writer);
459   xmlTextWriterEndElement (w->writer);
460   xmlTextWriterEndElement (w->writer);
461
462   /* <languages/> */
463   /* XXX should use the real language */
464   xmlTextWriterStartElement (w->writer, _xml ("languages"));
465   write_attr (w->writer, "base", "EN-US");
466   xmlTextWriterStartElement (w->writer, _xml ("language"));
467   write_attr (w->writer, "name", "EN-US");
468   write_attr (w->writer, "id", "0409");
469   xmlTextWriterEndElement (w->writer);
470   write_empty_element (w->writer, "deleted");
471   xmlTextWriterEndElement (w->writer);
472
473   /* <contexts/> */
474   xmlTextWriterStartElement (w->writer, _xml ("contexts"));
475   write_attr (w->writer, "base", "Analysis");
476   write_context (w->writer, "ANALYSIS", "QUESTION");
477   write_context (w->writer, "QUESTION", "ANALYSIS");
478   write_context (w->writer, "WEBAPP", NULL);
479   write_empty_element (w->writer, "deleted");
480   xmlTextWriterEndElement (w->writer);
481
482   /* <labeltypes/> */
483   xmlTextWriterStartElement (w->writer, _xml ("labeltypes"));
484   write_attr (w->writer, "base", "label");
485   write_context (w->writer, "LABEL", NULL);
486   write_empty_element (w->writer, "deleted");
487   xmlTextWriterEndElement (w->writer);
488
489   /* <routingcontexts/> */
490   write_empty_element (w->writer, "routingcontexts");
491
492   /* <scripttypes/> */
493   xmlTextWriterStartElement (w->writer, _xml ("scripttypes"));
494   write_attr (w->writer, "base", "mrScriptBasic");
495   write_context (w->writer, "MRSCRIPTBASIC", NULL);
496   write_empty_element (w->writer, "deleted");
497   xmlTextWriterEndElement (w->writer);
498
499   /* <versionlist/> */
500   write_empty_element (w->writer, "versionlist");
501
502   /* <categorymap/> */
503   xmlTextWriterStartElement (w->writer, _xml ("categorymap"));
504   struct string_set categories = STRING_SET_INITIALIZER (categories);
505   for (size_t i = 0; i < n_vars; i++)
506     {
507       const struct variable *var = dict_get_var (dict, i);
508       const struct val_labs *val_labs = var_get_value_labels (var);
509       size_t n_vls = val_labs_count (val_labs);
510       if (n_vls)
511         {
512           const struct val_lab **vls = val_labs_sorted (val_labs);
513
514           for (size_t j = 0; j < n_vls; j++)
515             {
516               const struct val_lab *vl = vls[j];
517
518               char *label = name_to_id (val_lab_get_label (vl));
519               if (string_set_insert_nocopy (&categories, label))
520                 {
521                   xmlTextWriterStartElement (w->writer, _xml ("categoryid"));
522                   write_attr (w->writer, "name", label);
523                   xmlTextWriterWriteFormatAttribute (
524                     w->writer, _xml ("value"),
525                     "%zu", string_set_count (&categories));
526                   xmlTextWriterEndElement (w->writer);
527                 }
528             }
529
530           free (vls);
531         }
532     }
533   string_set_destroy (&categories);
534   xmlTextWriterEndElement (w->writer);
535
536   /* <savelogs/> */
537   xmlTextWriterStartElement (w->writer, _xml ("savelogs"));
538   xmlTextWriterStartElement (w->writer, _xml ("savelog"));
539   write_attr (w->writer, "fileversion", "7.0.0.0.331");
540   write_attr (w->writer, "versionset", "");
541   write_attr (w->writer, "username", "Administrator");
542   time_t t;
543   if (time (&t) == (time_t) -1)
544     write_attr (w->writer, "date", "01/01/1970 00:00:00 AM");
545   else
546     {
547       struct tm *tm = localtime (&t);
548       int hour = tm->tm_hour % 12;
549       xmlTextWriterWriteFormatAttribute (w->writer, _xml ("date"),
550                                          "%02d/%02d/%04d %02d:%02d:%02d %s",
551                                          tm->tm_mon + 1, tm->tm_mday,
552                                          tm->tm_year + 1900,
553                                          hour ? hour : 12, tm->tm_min,
554                                          tm->tm_sec,
555                                          tm->tm_hour < 12 ? "AM" : "PM");
556     }
557   xmlTextWriterStartElement (w->writer, _xml ("user"));
558   write_attr (w->writer, "name", "pspp");
559   write_attr (w->writer, "fileversion", version);
560   write_attr (w->writer, "comment", "Written by GNU PSPP");
561   xmlTextWriterEndElement (w->writer);
562   xmlTextWriterEndElement (w->writer);
563   xmlTextWriterEndElement (w->writer);
564
565   /* </xml> */
566   xmlTextWriterEndElement (w->writer);
567
568   xmlTextWriterEndDocument (w->writer);
569
570 error:
571   mdd_close (w);
572   return NULL;
573 }