New command SAVE DATA COLLECTION.
authorBen Pfaff <blp@cs.stanford.edu>
Fri, 20 Apr 2018 06:00:41 +0000 (23:00 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Mon, 30 Apr 2018 00:52:44 +0000 (17:52 -0700)
Requested by Dave Trollope.

NEWS
doc/files.texi
src/data/automake.mk
src/data/mdd-writer.c [new file with mode: 0644]
src/data/mdd-writer.h [new file with mode: 0644]
src/language/command.def
src/language/data-io/save.c
tests/atlocal.in
tests/automake.mk

diff --git a/NEWS b/NEWS
index 7b943b15fb1dd8fb7fe8d9ef0426895b929ad51e..8bb992571f1cebf5ca0b3a8bd2841ff2278504dc 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -6,6 +6,8 @@ Please send PSPP bug reports to bug-gnu-pspp@gnu.org.
 
 Changes from 1.0.1 to 1.1.0:
 
+ * New experimental command SAVE DATA COLLECTION to save MDD files.
+
  * Build changes:
 
    - zlib is now a required dependency.  (Previously it was optional.)
index ede6e37d78f050ab3858db482348a44751f97e55..4085e386af34d51f17dfa4d1ec228b6f7b0eb637 100644 (file)
@@ -20,6 +20,7 @@ portable files.
 * GET DATA::                    Read from foreign files.
 * IMPORT::                      Read from a portable file.
 * SAVE::                        Write to a system file.
+* SAVE DATA COLLECTION::        Write to a system file and metadata file.
 * SAVE TRANSLATE::              Write data in foreign file formats.
 * SYSFILE INFO::                Display system file dictionary.
 * XEXPORT::                     Write to a portable file, as a transformation.
@@ -770,6 +771,45 @@ The @subcmd{NAMES} and @subcmd{MAP} subcommands are currently ignored.
 
 @cmd{SAVE} causes the data to be read.  It is a procedure.
 
+@node SAVE DATA COLLECTION
+@section SAVE DATA COLLECTION
+@vindex SAVE DATA COLLECTION
+
+@display
+SAVE DATA COLLECTION
+        /OUTFILE=@{'@var{file_name}',@var{file_handle}@}
+        /METADATA=@{'@var{file_name}',@var{file_handle}@}
+        /@{UNCOMPRESSED,COMPRESSED,ZCOMPRESSED@}
+        /PERMISSIONS=@{WRITEABLE,READONLY@}
+        /DROP=@var{var_list}
+        /KEEP=@var{var_list}
+        /VERSION=@var{version}
+        /RENAME=(@var{src_names}=@var{target_names})@dots{}
+        /NAMES
+        /MAP
+@end display
+
+Like @cmd{SAVE}, @cmd{SAVE DATA COLLECTION} writes the dictionary and
+data in the active dataset to a system file.  In addition, it writes
+metadata to an additional XML metadata file.
+
+OUTFILE is required.  Specify the system file to be written as a
+string file name or a file handle (@pxref{File Handles}).
+
+METADATA is also required.  Specify the metadata file to be written as
+a string file name or a file handle.  Metadata files customarily use a
+@file{.mdd} extension.
+
+The current implementation of this command is experimental.  It only
+outputs an approximation of the metadata file format.  Please report
+bugs.
+
+Other subcommands are optional.  They have the same meanings as in the
+@cmd{SAVE} command.
+
+@cmd{SAVE DATA COLLECTION} causes the data to be read.  It is a
+procedure.
+
 @node SAVE TRANSLATE
 @section SAVE TRANSLATE
 @vindex SAVE TRANSLATE
index 1572eea83e9a51fca409f2194aefd2015737415c..b59423a1532c06e1bdd17086f13a424bc04e5554 100644 (file)
@@ -92,6 +92,8 @@ src_data_libdata_la_SOURCES = \
        src/data/identifier.h \
        src/data/lazy-casereader.c \
        src/data/lazy-casereader.h \
+       src/data/mdd-writer.c \
+       src/data/mdd-writer.h \
        src/data/missing-values.c \
        src/data/missing-values.h \
        src/data/make-file.c \
diff --git a/src/data/mdd-writer.c b/src/data/mdd-writer.c
new file mode 100644 (file)
index 0000000..a4d4e26
--- /dev/null
@@ -0,0 +1,573 @@
+/* PSPP - a program for statistical analysis.
+   Copyright (C) 2018 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program 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 General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#include "data/mdd-writer.h"
+
+#include <errno.h>
+#include <libxml/xmlwriter.h>
+#include <stdbool.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <time.h>
+
+#include "data/dictionary.h"
+#include "data/file-handle-def.h"
+#include "data/make-file.h"
+#include "data/short-names.h"
+#include "data/value-labels.h"
+#include "data/variable.h"
+#include "libpspp/message.h"
+#include "libpspp/misc.h"
+#include "libpspp/string-set.h"
+#include "libpspp/version.h"
+
+#include "gl/c-ctype.h"
+#include "gl/ftoastr.h"
+#include "gl/xalloc.h"
+#include "gl/xmemdup0.h"
+
+#include "gettext.h"
+#define _(msgid) gettext (msgid)
+#define N_(msgid) (msgid)
+
+#define _xml(X) CHAR_CAST (const xmlChar *, X)
+
+/* Metadata file writer. */
+struct mdd_writer
+  {
+    struct file_handle *fh;     /* File handle. */
+    struct fh_lock *lock;       /* Mutual exclusion for file. */
+    FILE *file;                        /* File stream. */
+    struct replace_file *rf;    /* Ticket for replacing output file. */
+
+    xmlTextWriter *writer;
+  };
+
+/* Returns true if an I/O error has occurred on WRITER, false otherwise. */
+static bool
+mdd_write_error (const struct mdd_writer *writer)
+{
+  return ferror (writer->file);
+}
+
+static bool
+mdd_close (struct mdd_writer *w)
+{
+  if (!w)
+    return true;
+
+  if (w->writer)
+    xmlFreeTextWriter (w->writer);
+
+  bool ok = true;
+  if (w->file)
+    {
+      fflush (w->file);
+
+      ok = !mdd_write_error (w);
+      if (fclose (w->file) == EOF)
+        ok = false;
+
+      if (!ok)
+        msg (ME, _("An I/O error occurred writing metadata file `%s'."),
+             fh_get_file_name (w->fh));
+
+      if (ok ? !replace_file_commit (w->rf) : !replace_file_abort (w->rf))
+        ok = false;
+    }
+
+  fh_unlock (w->lock);
+  fh_unref (w->fh);
+
+  free (w);
+
+  return ok;
+}
+
+static void
+write_empty_element (xmlTextWriter *writer, const char *name)
+{
+  xmlTextWriterStartElement (writer, _xml (name));
+  xmlTextWriterEndElement (writer);
+}
+
+static void
+write_attr (xmlTextWriter *writer, const char *key, const char *value)
+{
+  xmlTextWriterWriteAttribute (writer, _xml (key), _xml (value));
+}
+
+static void
+write_global_name_space (xmlTextWriter *writer)
+{
+  write_attr (writer, "global-name-space", "-1");
+}
+
+static void
+write_xml_lang (xmlTextWriter *writer)
+{
+  /* XXX should write real language */
+  xmlTextWriterWriteAttributeNS (writer, _xml ("xml"), _xml ("lang"), NULL,
+                                 _xml ("en-US"));
+}
+
+static void
+write_value_label_value (xmlTextWriter *writer, const struct val_lab *vl,
+                         int width)
+{
+  /* XXX below would better use syntax_gen_value(). */
+  const union value *value = val_lab_get_value (vl);
+  if (width)
+    {
+      char *s = xmemdup0 (value_str (value, width), width);
+      xmlTextWriterWriteAttribute (writer, _xml ("value"), _xml (s));
+      free (s);
+    }
+  else
+    {
+      char s[DBL_BUFSIZE_BOUND];
+
+      c_dtoastr (s, sizeof s, 0, 0, value->f);
+      xmlTextWriterWriteAttribute (writer, _xml ("value"), _xml (s));
+    }
+}
+
+static void
+write_context (xmlTextWriter *writer, const char *name,
+               const char *alternative)
+{
+  xmlTextWriterStartElement (writer, _xml ("context"));
+  write_attr (writer, "name", name);
+  if (alternative)
+    {
+      xmlTextWriterStartElement (writer, _xml ("alternatives"));
+      xmlTextWriterStartElement (writer, _xml ("alternative"));
+      write_attr (writer, "name", alternative);
+      xmlTextWriterEndElement (writer);
+      write_empty_element (writer, "deleted");
+      xmlTextWriterEndElement (writer);
+    }
+  xmlTextWriterEndElement (writer);
+}
+
+static char *
+name_to_id (const char *name)
+{
+  char *id = xmalloc (strlen (name) + 2);
+  char *d = id;
+  for (const char *s = name; *s; s++)
+    {
+      if (c_isalpha (*s))
+        *d++ = c_tolower (*s);
+      else if (c_isdigit (*s))
+        {
+          if (d == id)
+            *d++ = '_';
+          *d++ = *s;
+        }
+      else
+        {
+          if (d == id || d[-1] != '_')
+            *d++ = '_';
+        }
+    }
+  if (d > id && d[-1] == '_')
+    d--;
+  *d = '\0';
+
+  return id;
+}
+
+bool
+mdd_write (struct file_handle *fh, struct dictionary *dict,
+           const char *sav_name)
+{
+  struct mdd_writer *w = xzalloc (sizeof *w);
+
+  /* Open file handle as an exclusive writer. */
+  /* TRANSLATORS: this fragment will be interpolated into
+     messages in fh_lock() that identify types of files. */
+  w->lock = fh_lock (fh, FH_REF_FILE, N_("metadata file"), FH_ACC_WRITE, true);
+  if (w->lock == NULL)
+    goto error;
+
+  /* Create the file on disk. */
+  w->rf = replace_file_start (fh, "wb", 0444, &w->file);
+  if (w->rf == NULL)
+    {
+      msg (ME, _("Error opening `%s' for writing as a metadata file: %s."),
+           fh_get_file_name (fh), strerror (errno));
+      goto error;
+    }
+
+  w->writer = xmlNewTextWriter (xmlOutputBufferCreateFile (w->file, NULL));
+  if (!w->writer)
+    {
+      msg (ME, _("Internal error creating xmlTextWriter."));
+      goto error;
+    }
+
+  xmlTextWriterStartDocument (w->writer, NULL, "UTF-8", NULL);
+
+  /* The MDD file contents seem to roughly correspond to the object model
+     documentatation at:
+     https://support.unicomsi.com/manuals/intelligence/75/DDL/MDM/docjet/metadata/chm/contentsf.html.  */
+
+  /* <?xml-stylesheet type="text/xsl" href="mdd.xslt"?> */
+  xmlTextWriterStartPI (w->writer, _xml ("xml-stylesheet"));
+  xmlTextWriterWriteString (w->writer,
+                            _xml ("type=\"text/xsl\" href=\"mdd.xslt\""));
+  xmlTextWriterEndPI (w->writer);
+
+  xmlTextWriterStartElement (w->writer, _xml ("xml"));
+
+  /* <mdm:metadata xmlns:mdm="http://www.spss.com/mr/dm/metadatamodel/Arc
+     3/2000-02-04" mdm_createversion="7.0.0.0.331"
+     mdm_lastversion="7.0.0.0.331" id="c4c181c1-0d7c-42e3-abcd-f08296d1dfdc"
+     data_version="9" data_sub_version="1" systemvariable="0"
+     dbfiltervalidation="-1"> */
+  xmlTextWriterStartElementNS (
+    w->writer, _xml ("mdm"), _xml ("metadata"),
+    _xml ("http://www.spss.com/mr/dm/metadatamodel/Arc%203/2000-02-04"));
+  static const struct pair
+    {
+      const char *key, *value;
+    }
+  pairs[] =
+    {
+      { "mdm_createversion", "7.0.0.0.331" },
+      { "mdm_lastversion", "7.0.0.0.331" },
+      { "id", "c4c181c1-0d7c-42e3-abcd-f08296d1dfdc" },
+      { "data_version", "9" },
+      { "data_sub_version", "1" },
+      { "systemvariable", "0" },
+      { "dbfiltervalidation", "-1" },
+    };
+  const int n_pairs = sizeof pairs / sizeof *pairs;
+  for (const struct pair *p = pairs; p < &pairs[n_pairs]; p++)
+    xmlTextWriterWriteAttribute (w->writer, _xml (p->key), _xml (p->value));
+
+  /* <atoms/> */
+  xmlTextWriterStartElement (w->writer, _xml ("atoms"));
+  /* XXX Real files contain a list of languages and a few other random strings
+     here in <atom name="..."/> elements.  It's really not clear what they're
+     good for. */
+  xmlTextWriterEndElement (w->writer);
+
+  /* <datasources> */
+  xmlTextWriterStartElement (w->writer, _xml ("datasources"));
+  xmlTextWriterWriteAttribute (w->writer, _xml ("default"), _xml ("mrSavDsc"));
+
+  /* <connection/> */
+  xmlTextWriterStartElement (w->writer, _xml ("connection"));
+  write_attr (w->writer, "name", "mrSavDsc");
+  write_attr (w->writer, "dblocation", sav_name);
+  write_attr (w->writer, "cdscname", "mrSavDsc");
+  write_attr (w->writer, "project", "126");
+
+  size_t n_vars = dict_get_var_cnt (dict);
+  short_names_assign (dict);
+  for (size_t i = 0; i < n_vars; i++)
+    {
+      const struct variable *var = dict_get_var (dict, i);
+      xmlTextWriterStartElement (w->writer, _xml ("var"));
+
+      char *short_name = xstrdup (var_get_short_name (var, 0));
+      for (char *p = short_name; *p; p++)
+        *p = c_tolower (*p);
+      write_attr (w->writer, "fullname", short_name);
+      free (short_name);
+
+      write_attr (w->writer, "aliasname", var_get_name (var));
+
+      const struct val_labs *val_labs = var_get_value_labels (var);
+      size_t n_vls = val_labs_count (val_labs);
+      if (n_vls)
+        {
+          const struct val_lab **vls = val_labs_sorted (val_labs);
+
+          xmlTextWriterStartElement (w->writer, _xml ("nativevalues"));
+          int width = var_get_width (var);
+          for (size_t j = 0; j < n_vls; j++)
+            {
+              const struct val_lab *vl = vls[j];
+              xmlTextWriterStartElement (w->writer, _xml ("nativevalue"));
+
+              char *fullname = name_to_id (val_lab_get_label (vl));
+              write_attr (w->writer, "fullname", fullname);
+              free (fullname);
+
+              write_value_label_value (w->writer, vl, width);
+              xmlTextWriterEndElement (w->writer);
+            }
+          xmlTextWriterEndElement (w->writer);
+
+          free (vls);
+        }
+
+      xmlTextWriterEndElement (w->writer);
+    }
+  xmlTextWriterEndElement (w->writer);
+  xmlTextWriterEndElement (w->writer);
+
+  /* We reserve ids 1...N_VARS for variables and then start other ids after
+     that. */
+  int id = dict_get_var_cnt (dict) + 1;
+
+  /* <definition/> */
+  xmlTextWriterStartElement (w->writer, _xml ("definition"));
+  for (size_t i = 0; i < n_vars; i++)
+    {
+      const struct variable *var = dict_get_var (dict, i);
+      xmlTextWriterStartElement (w->writer, _xml ("variable"));
+      xmlTextWriterWriteFormatAttribute (w->writer, _xml ("id"), "%zu", i + 1);
+      write_attr (w->writer, "name", var_get_name (var));
+
+      bool is_string = var_get_type (var) == VAL_STRING;
+      int type = is_string ? 2 : 3;
+      xmlTextWriterWriteFormatAttribute (w->writer, _xml ("type"), "%d", type);
+
+      int max = is_string ? var_get_width (var) : 1;
+      xmlTextWriterWriteFormatAttribute (w->writer, _xml ("max"), "%d", max);
+
+      write_attr (w->writer, "maxtype", "3");
+
+      const char *label = var_get_label (var);
+      if (label)
+        {
+          xmlTextWriterStartElement (w->writer, _xml ("labels"));
+          write_attr (w->writer, "context", "LABEL");
+
+          xmlTextWriterStartElement (w->writer, _xml ("text"));
+          write_attr (w->writer, "context", "ANALYSIS");
+          write_xml_lang (w->writer);
+          xmlTextWriterWriteString (w->writer, _xml (label));
+          xmlTextWriterEndElement (w->writer);
+
+          xmlTextWriterEndElement (w->writer);
+        }
+
+      const struct val_labs *val_labs = var_get_value_labels (var);
+      size_t n_vls = val_labs_count (val_labs);
+      if (n_vls)
+        {
+          const struct val_lab **vls = val_labs_sorted (val_labs);
+
+          /* <categories/> */
+          xmlTextWriterStartElement (w->writer, _xml ("categories"));
+          write_global_name_space (w->writer);
+          int width = var_get_width (var);
+          for (size_t j = 0; j < n_vls; j++)
+            {
+              const struct val_lab *vl = vls[j];
+
+              /* <category> */
+              xmlTextWriterStartElement (w->writer, _xml ("category"));
+              xmlTextWriterWriteFormatAttribute (w->writer, _xml ("id"),
+                                                 "_%d", id++);
+
+              char *name = name_to_id (val_lab_get_label (vl));
+              write_attr (w->writer, "name", name);
+              free (name);
+
+              /* <properties/> */
+              xmlTextWriterStartElement (w->writer, _xml ("properties"));
+              xmlTextWriterStartElement (w->writer, _xml ("property"));
+              write_attr (w->writer, "name", "Value");
+              write_value_label_value (w->writer, vl, width);
+              write_attr (w->writer, "type", "5");
+              write_attr (w->writer, "context", "Analysis");
+              xmlTextWriterEndElement (w->writer);
+              xmlTextWriterEndElement (w->writer);
+
+              /* <labels/> */
+              xmlTextWriterStartElement (w->writer, _xml ("labels"));
+              write_attr (w->writer, "context", "LABEL");
+              xmlTextWriterStartElement (w->writer, _xml ("text"));
+              write_attr (w->writer, "context", "ANALYSIS");
+              write_xml_lang (w->writer);
+              xmlTextWriterWriteString (w->writer,
+                                        _xml (val_lab_get_label (vl)));
+              xmlTextWriterEndElement (w->writer);
+              xmlTextWriterEndElement (w->writer);
+
+
+              /* </category> */
+              xmlTextWriterEndElement (w->writer);
+            }
+          write_empty_element (w->writer, "deleted");
+          xmlTextWriterEndElement (w->writer);
+
+          free (vls);
+        }
+
+      xmlTextWriterEndElement (w->writer);
+    }
+  xmlTextWriterEndElement (w->writer);
+
+  write_empty_element (w->writer, "system");
+  write_empty_element (w->writer, "systemrouting");
+  write_empty_element (w->writer, "mappings");
+
+  /* <design/> */
+  xmlTextWriterStartElement (w->writer, _xml ("design"));
+  xmlTextWriterStartElement (w->writer, _xml ("fields"));
+  write_attr (w->writer, "name", "@fields");
+  write_global_name_space (w->writer);
+  for (size_t i = 0; i < n_vars; i++)
+    {
+      const struct variable *var = dict_get_var (dict, i);
+      xmlTextWriterStartElement (w->writer, _xml ("variable"));
+      xmlTextWriterWriteFormatAttribute (w->writer, _xml ("id"),
+                                         "_%d", i + 1);
+      write_attr (w->writer, "name", var_get_name (var));
+      xmlTextWriterWriteFormatAttribute (w->writer, _xml ("ref"),
+                                         "%d", i + 1);
+      xmlTextWriterEndElement (w->writer);
+    }
+  write_empty_element (w->writer, "deleted");
+  xmlTextWriterEndElement (w->writer);
+  xmlTextWriterStartElement (w->writer, _xml ("types"));
+  write_attr (w->writer, "name", "@types");
+  write_global_name_space (w->writer);
+  write_empty_element (w->writer, "deleted");
+  xmlTextWriterEndElement (w->writer);
+  xmlTextWriterStartElement (w->writer, _xml ("pages"));
+  write_attr (w->writer, "name", "@pages");
+  write_global_name_space (w->writer);
+  write_empty_element (w->writer, "deleted");
+  xmlTextWriterEndElement (w->writer);
+  xmlTextWriterStartElement (w->writer, _xml ("routings"));
+  xmlTextWriterStartElement (w->writer, _xml ("scripts"));
+  write_empty_element (w->writer, "deleted");
+  xmlTextWriterEndElement (w->writer);
+  xmlTextWriterEndElement (w->writer);
+  xmlTextWriterEndElement (w->writer);
+
+  /* <languages/> */
+  /* XXX should use the real language */
+  xmlTextWriterStartElement (w->writer, _xml ("languages"));
+  write_attr (w->writer, "base", "EN-US");
+  xmlTextWriterStartElement (w->writer, _xml ("language"));
+  write_attr (w->writer, "name", "EN-US");
+  write_attr (w->writer, "id", "0409");
+  xmlTextWriterEndElement (w->writer);
+  write_empty_element (w->writer, "deleted");
+  xmlTextWriterEndElement (w->writer);
+
+  /* <contexts/> */
+  xmlTextWriterStartElement (w->writer, _xml ("contexts"));
+  write_attr (w->writer, "base", "Analysis");
+  write_context (w->writer, "ANALYSIS", "QUESTION");
+  write_context (w->writer, "QUESTION", "ANALYSIS");
+  write_context (w->writer, "WEBAPP", NULL);
+  write_empty_element (w->writer, "deleted");
+  xmlTextWriterEndElement (w->writer);
+
+  /* <labeltypes/> */
+  xmlTextWriterStartElement (w->writer, _xml ("labeltypes"));
+  write_attr (w->writer, "base", "label");
+  write_context (w->writer, "LABEL", NULL);
+  write_empty_element (w->writer, "deleted");
+  xmlTextWriterEndElement (w->writer);
+
+  /* <routingcontexts/> */
+  write_empty_element (w->writer, "routingcontexts");
+
+  /* <scripttypes/> */
+  xmlTextWriterStartElement (w->writer, _xml ("scripttypes"));
+  write_attr (w->writer, "base", "mrScriptBasic");
+  write_context (w->writer, "MRSCRIPTBASIC", NULL);
+  write_empty_element (w->writer, "deleted");
+  xmlTextWriterEndElement (w->writer);
+
+  /* <versionlist/> */
+  write_empty_element (w->writer, "versionlist");
+
+  /* <categorymap/> */
+  xmlTextWriterStartElement (w->writer, _xml ("categorymap"));
+  struct string_set categories = STRING_SET_INITIALIZER (categories);
+  for (size_t i = 0; i < n_vars; i++)
+    {
+      const struct variable *var = dict_get_var (dict, i);
+      const struct val_labs *val_labs = var_get_value_labels (var);
+      size_t n_vls = val_labs_count (val_labs);
+      if (n_vls)
+        {
+          const struct val_lab **vls = val_labs_sorted (val_labs);
+
+          for (size_t j = 0; j < n_vls; j++)
+            {
+              const struct val_lab *vl = vls[j];
+
+              char *label = name_to_id (val_lab_get_label (vl));
+              if (string_set_insert_nocopy (&categories, label))
+                {
+                  xmlTextWriterStartElement (w->writer, _xml ("categoryid"));
+                  write_attr (w->writer, "name", label);
+                  xmlTextWriterWriteFormatAttribute (
+                    w->writer, _xml ("value"),
+                    "%d", string_set_count (&categories));
+                  xmlTextWriterEndElement (w->writer);
+                }
+            }
+
+          free (vls);
+        }
+    }
+  string_set_destroy (&categories);
+  xmlTextWriterEndElement (w->writer);
+
+  /* <savelogs/> */
+  xmlTextWriterStartElement (w->writer, _xml ("savelogs"));
+  xmlTextWriterStartElement (w->writer, _xml ("savelog"));
+  write_attr (w->writer, "fileversion", "7.0.0.0.331");
+  write_attr (w->writer, "versionset", "");
+  write_attr (w->writer, "username", "Administrator");
+  time_t t;
+  if (time (&t) == (time_t) -1)
+    write_attr (w->writer, "date", "01/01/1970 00:00:00 AM");
+  else
+    {
+      struct tm *tm = localtime (&t);
+      int hour = tm->tm_hour % 12;
+      xmlTextWriterWriteFormatAttribute (w->writer, _xml ("date"),
+                                         "%02d/%02d/%04d %02d:%02d:%02d %s",
+                                         tm->tm_mon + 1, tm->tm_mday,
+                                         tm->tm_year + 1900,
+                                         hour ? hour : 12, tm->tm_min,
+                                         tm->tm_sec,
+                                         tm->tm_hour < 12 ? "AM" : "PM");
+    }
+  xmlTextWriterStartElement (w->writer, _xml ("user"));
+  write_attr (w->writer, "name", "pspp");
+  write_attr (w->writer, "fileversion", version);
+  write_attr (w->writer, "comment", "Written by GNU PSPP");
+  xmlTextWriterEndElement (w->writer);
+  xmlTextWriterEndElement (w->writer);
+  xmlTextWriterEndElement (w->writer);
+
+  /* </xml> */
+  xmlTextWriterEndElement (w->writer);
+
+  xmlTextWriterEndDocument (w->writer);
+
+error:
+  mdd_close (w);
+  return NULL;
+}
diff --git a/src/data/mdd-writer.h b/src/data/mdd-writer.h
new file mode 100644 (file)
index 0000000..949ce52
--- /dev/null
@@ -0,0 +1,29 @@
+/* PSPP - a program for statistical analysis.
+   Copyright (C) 2018 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program 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 General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+#ifndef MDD_WRITER_H
+#define MDD_WRITER_H 1
+
+#include <stdbool.h>
+
+/* Writing MDD files. */
+
+struct file_handle;
+struct dictionary;
+bool mdd_write (struct file_handle *, struct dictionary *,
+                const char *sav_name);
+
+#endif /* mdd-writer.h */
index f2d50331852242cad8b59fe9e348f1fda315de6d..a97f9b83e70fd1c7e021188eb6a84107a6c04627 100644 (file)
@@ -139,6 +139,7 @@ DEF_CMD (S_DATA, 0, "RENAME VARIABLES", cmd_rename_variables)
 DEF_CMD (S_DATA, 0, "ROC", cmd_roc)
 DEF_CMD (S_DATA, 0, "SAMPLE", cmd_sample)
 DEF_CMD (S_DATA, 0, "SAVE", cmd_save)
+DEF_CMD (S_DATA, 0, "SAVE DATA COLLECTION", cmd_save_data_collection)
 DEF_CMD (S_DATA, 0, "SAVE TRANSLATE", cmd_save_translate)
 DEF_CMD (S_DATA, 0, "SORT CASES", cmd_sort_cases)
 DEF_CMD (S_DATA, 0, "SORT VARIABLES", cmd_sort_variables)
index b97da69b00eff3a278aeb18762820f7c76ac15c2..cec878766ad07b2375c37afe5c044583ef8d834a 100644 (file)
@@ -25,6 +25,7 @@
 #include "data/casewriter.h"
 #include "data/dataset.h"
 #include "data/dictionary.h"
+#include "data/mdd-writer.h"
 #include "data/por-file-writer.h"
 #include "data/sys-file-writer.h"
 #include "data/transformations.h"
@@ -68,6 +69,12 @@ cmd_save (struct lexer *lexer, struct dataset *ds)
   return parse_output_proc (lexer, ds, SYSFILE_WRITER);
 }
 
+int
+cmd_save_data_collection (struct lexer *lexer, struct dataset *ds)
+{
+  return parse_output_proc (lexer, ds, SYSFILE_WRITER);
+}
+
 int
 cmd_export (struct lexer *lexer, struct dataset *ds)
 {
@@ -154,6 +161,7 @@ parse_write_command (struct lexer *lexer, struct dataset *ds,
 {
   /* Common data. */
   struct file_handle *handle; /* Output file. */
+  struct file_handle *metadata; /* MDD output file. */
   struct dictionary *dict;    /* Dictionary for output file. */
   struct casewriter *writer;  /* Writer. */
   struct case_map_stage *stage; /* Preparation for 'map'. */
@@ -171,6 +179,7 @@ parse_write_command (struct lexer *lexer, struct dataset *ds,
     *retain_unselected = true;
 
   handle = NULL;
+  metadata = NULL;
   dict = dict_clone (dataset_dict (ds));
   writer = NULL;
   stage = NULL;
@@ -198,6 +207,20 @@ parse_write_command (struct lexer *lexer, struct dataset *ds,
          if (handle == NULL)
            goto error;
        }
+      else if (lex_match_id (lexer, "METADATA"))
+       {
+          if (metadata != NULL)
+            {
+              lex_sbc_only_once ("METADATA");
+              goto error;
+            }
+
+         lex_match (lexer, T_EQUALS);
+
+         metadata = fh_parse (lexer, FH_REF_FILE, NULL);
+         if (metadata == NULL)
+           goto error;
+       }
       else if (lex_match_id (lexer, "NAMES"))
         {
           /* Not yet implemented. */
@@ -306,6 +329,15 @@ parse_write_command (struct lexer *lexer, struct dataset *ds,
   if (writer == NULL)
     goto error;
 
+  if (metadata)
+    {
+      const char *sav_name = (fh_get_referent (handle) == FH_REF_FILE
+                              ? fh_get_file_name (handle)
+                              : fh_get_name (handle));
+      if (!mdd_write (metadata, dict, sav_name))
+        goto error;
+    }
+
   map = case_map_stage_get_case_map (stage);
   case_map_stage_destroy (stage);
   if (map != NULL)
@@ -313,11 +345,13 @@ parse_write_command (struct lexer *lexer, struct dataset *ds,
   dict_destroy (dict);
 
   fh_unref (handle);
+  fh_unref (metadata);
   return writer;
 
  error:
   case_map_stage_destroy (stage);
   fh_unref (handle);
+  fh_unref (metadata);
   casewriter_destroy (writer);
   dict_destroy (dict);
   case_map_destroy (map);
index cd3b8e2ada47e3bf10eb03dd524abd4b4fc00847..041cf289a806e8b9a3aac165f7fadd8d4d989438 100644 (file)
@@ -26,6 +26,13 @@ host='@host@'
 PACKAGE_STRING='@PACKAGE_STRING@'
 PACKAGE_BUGREPORT='@PACKAGE_BUGREPORT@'
 
+XMLLINT='@XMLLINT@'
+if echo | $XMLLINT - >/dev/null 2>&1; then
+    HAVE_XMLLINT=no
+else
+    HAVE_XMLLINT=yes
+fi
+
 PSQL_SUPPORT='@PSQL_SUPPORT@'
 if test "$PSQL_SUPPORT" = yes; then
     : ${PG_CONFIG:='@PG_CONFIG@'}
index 8d6d85fd7cb4541ec9183e8fa9468cdd3fe754d8..4a05bea98087dc66e661343a2586f9ea4ad31858 100644 (file)
@@ -300,6 +300,7 @@ TESTSUITE_AT = \
        tests/data/dictionary.at \
        tests/data/file.at \
        tests/data/format-guesser.at \
+       tests/data/mdd-file.at \
        tests/data/pc+-file-reader.at \
        tests/data/por-file.at \
        tests/data/sys-file-reader.at \