]> git.ipfire.org Git - thirdparty/gettext.git/commitdiff
Java format string checking.
authorBruno Haible <bruno@clisp.org>
Mon, 27 Aug 2001 12:04:15 +0000 (12:04 +0000)
committerBruno Haible <bruno@clisp.org>
Mon, 27 Aug 2001 12:04:15 +0000 (12:04 +0000)
src/format-java.c [new file with mode: 0644]
tests/format-java-1 [new file with mode: 0755]
tests/format-java-2 [new file with mode: 0755]

diff --git a/src/format-java.c b/src/format-java.c
new file mode 100644 (file)
index 0000000..fbc5549
--- /dev/null
@@ -0,0 +1,779 @@
+/* Java format strings.
+   Copyright (C) 2001 Free Software Foundation, Inc.
+   Written by Bruno Haible <haible@clisp.cons.org>, 2001.
+
+   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 2, 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, write to the Free Software Foundation,
+   Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "format.h"
+#include "c-ctype.h"
+#include "system.h"
+#include "error.h"
+#include "progname.h"
+#include "libgettext.h"
+
+#define _(str) gettext (str)
+
+/* Java format strings are described in java/text/MessageFormat.html.
+   See also the ICU documentation class_MessageFormat.html.
+
+   messageFormatPattern := string ( "{" messageFormatElement "}" string )*
+
+   messageFormatElement := argument { "," elementFormat }
+
+   elementFormat := "time" { "," datetimeStyle }
+                  | "date" { "," datetimeStyle }
+                  | "number" { "," numberStyle }
+                  | "choice" { "," choiceStyle }
+
+   datetimeStyle := "short"
+                    | "medium"
+                    | "long"
+                    | "full"
+                    | dateFormatPattern
+
+   numberStyle := "currency"
+                 | "percent"
+                 | "integer"
+                 | numberFormatPattern
+
+   choiceStyle := choiceFormatPattern
+
+   dateFormatPattern see SimpleDateFormat.applyPattern
+
+   numberFormatPattern see DecimalFormat.applyPattern
+
+   choiceFormatPattern see ChoiceFormat constructor
+
+   In strings, literal curly braces can be used if quoted between single
+   quotes.  A real single quote is represented by ''.
+
+   If a pattern is used, then unquoted braces in the pattern, if any, must
+   match: that is, "ab {0} de" and "ab '}' de" are ok, but "ab {0'}' de" and
+   "ab } de" are not.
+
+   The argument is a number from 0 to 9, which corresponds to the arguments
+   presented in an array to be formatted.
+
+   It is ok to have unused arguments in the array.
+
+   Adding a dateFormatPattern / numberFormatPattern / choiceFormatPattern
+   to an elementFormat is equivalent to creating a SimpleDateFormat /
+   DecimalFormat / ChoiceFormat and use of setFormat. For example,
+
+     MessageFormat form =
+       new MessageFormat("The disk \"{1}\" contains {0,choice,0#no files|1#one file|2#{0,number} files}.");
+
+   is equivalent to
+
+     MessageFormat form = new MessageFormat("The disk \"{1}\" contains {0}.");
+     form.setFormat(1, // Number of {} occurrence in the string!
+                    new ChoiceFormat(new double[] { 0, 1, 2 },
+                                     new String[] { "no files", "one file",
+                                                    "{0,number} files" }));
+
+   Note: The behaviour of quotes inside a choiceFormatPattern is not clear.
+   Example 1:
+     "abc{1,choice,0#{1,number,00';'000}}def"
+       JDK 1.1.x: exception
+       JDK 1.3.x: behaves like "abc{1,choice,0#{1,number,00;000}}def"
+   Example 2:
+     "abc{1,choice,0#{1,number,00';'}}def"
+       JDK 1.1.x: interprets the semicolon as number suffix
+       JDK 1.3.x: behaves like "abc{1,choice,0#{1,number,00;}}def"
+ */
+
+enum format_arg_type
+{
+  FAT_NONE,
+  FAT_OBJECT,  /* java.lang.Object */
+  FAT_NUMBER,  /* java.lang.Number */
+  FAT_DATE     /* java.util.Date */
+};
+
+struct numbered_arg
+{
+  unsigned int number;
+  enum format_arg_type type;
+};
+
+struct spec
+{
+  unsigned int directives;
+  unsigned int numbered_arg_count;
+  unsigned int allocated;
+  struct numbered_arg *numbered;
+};
+
+
+/* Prototypes for local functions.  Needed to ensure compiler checking of
+   function argument counts despite of K&R C function definition syntax.  */
+static bool message_format_parse PARAMS ((const char *format,
+                                         struct spec *spec));
+static bool date_format_parse PARAMS ((const char *format));
+static bool number_format_parse PARAMS ((const char *format));
+static bool choice_format_parse PARAMS ((const char *format,
+                                        struct spec *spec));
+static int numbered_arg_compare PARAMS ((const void *p1, const void *p2));
+static void *format_parse PARAMS ((const char *format));
+static void format_free PARAMS ((void *descr));
+static int format_get_number_of_directives PARAMS ((void *descr));
+static bool format_check PARAMS ((const lex_pos_ty *pos,
+                                 void *msgid_descr, void *msgstr_descr));
+
+
+/* Quote handling:
+   - When we see a single-quote, ignore it, but toggle the quoting flag.
+   - When we see a double single-quote, ignore the first of the two.
+   Assumes local variables format, quoting.  */
+#define HANDLE_QUOTE \
+  if (*format == '\'' && *++format != '\'') \
+    quoting = !quoting;
+
+/* Note that message_format_parse and choice_format_parse are mutually
+   recursive.  This is because MessageFormat can use some ChoiceFormats,
+   and a ChoiceFormat is made up from several MessageFormats.  */
+
+/* Return true if a format is a valid messageFormatPattern.
+   Extracts argument type information into spec.  */
+static bool
+message_format_parse (format, spec)
+     const char *format;
+     struct spec *spec;
+{
+  bool quoting = false;
+
+  for (;;)
+    {
+      HANDLE_QUOTE;
+      if (!quoting && *format == '{')
+       {
+         unsigned int depth;
+         const char *element_start;
+         const char *element_end;
+         size_t n;
+         char *element;
+         unsigned int number;
+         enum format_arg_type type;
+
+         spec->directives++;
+
+         element_start = ++format;
+         depth = 0;
+         for (; *format != '\0'; format++)
+           {
+             if (*format == '{')
+               depth++;
+             else if (*format == '}')
+               {
+                 if (depth == 0)
+                   break;
+                 else
+                   depth--;
+               }
+           }
+         if (*format == '\0')
+           return false;
+         element_end = format++;
+
+         n = element_end - element_start;
+         element = (char *) alloca (n + 1);
+         memcpy (element, element_start, n);
+         element[n] = '\0';
+
+         if (!c_isdigit (*element))
+           return false;
+         number = 0;
+         do
+           {
+             number = 10 * number + (*element - '0');
+             element++;
+           }
+         while (c_isdigit (*element));
+
+         type = FAT_OBJECT;
+         if (*element == '\0')
+           ;
+         else if (strncmp (element, ",time", 5) == 0
+                  || strncmp (element, ",date", 5) == 0)
+           {
+             type = FAT_DATE;
+             element += 5;
+             if (*element == '\0')
+               ;
+             else if (*element++ == ','
+                      && (strcmp (element, "short") == 0
+                          || strcmp (element, "medium") == 0
+                          || strcmp (element, "long") == 0
+                          || strcmp (element, "full") == 0
+                          || date_format_parse (element)))
+               ;
+             else
+               return false;
+           }
+         else if (strncmp (element, ",number", 7) == 0)
+           {
+             type = FAT_NUMBER;
+             element += 7;
+             if (*element == '\0')
+               ;
+             else if (*element++ == ','
+                      && (strcmp (element, "currency") == 0
+                          || strcmp (element, "percent") == 0
+                          || strcmp (element, "integer") == 0
+                          || number_format_parse (element)))
+               ;
+             else
+               return false;
+           }
+         else if (strncmp (element, ",choice", 7) == 0)
+           {
+             type = FAT_NUMBER; /* because ChoiceFormat extends NumberFormat */
+             element += 7;
+             if (*element == '\0')
+               ;
+             else if (*element++ == ','
+                      && choice_format_parse (element, spec))
+               ;
+             else
+               return false;
+           }
+         else
+           return false;
+
+         if (spec->allocated == spec->numbered_arg_count)
+           {
+             spec->allocated = 2 * spec->allocated + 1;
+             spec->numbered = (struct numbered_arg *) xrealloc (spec->numbered, spec->allocated * sizeof (struct numbered_arg));
+           }
+         spec->numbered[spec->numbered_arg_count].number = number;
+         spec->numbered[spec->numbered_arg_count].type = type;
+         spec->numbered_arg_count++;
+       }
+      /* The doc says "ab}de" is invalid.  Even though JDK accepts it.  */
+      else if (!quoting && *format == '}')
+       return false;
+      else if (*format != '\0')
+       format++;
+      else
+       break;
+    }
+
+  return true;
+}
+
+/* Return true if a format is a valid dateFormatPattern.  */
+static bool
+date_format_parse (format)
+     const char *format;
+{
+  /* Any string is valid.  Single-quote starts a quoted section, to be
+     terminated at the next single-quote or string end.  Double single-quote
+     gives a single single-quote.  Non-quoted ASCII letters are first grouped
+     into blocks of equal letters.  Then each block (e.g. 'yyyy') is
+     interpreted according to some rules.  */
+  return true;
+}
+
+/* Return true if a format is a valid numberFormatPattern.  */
+static bool
+number_format_parse (format)
+     const char *format;
+{
+  /* Pattern Syntax:
+       pattern     := pos_pattern{';' neg_pattern}
+       pos_pattern := {prefix}number{suffix}
+       neg_pattern := {prefix}number{suffix}
+       number      := integer{'.' fraction}{exponent}
+       prefix      := '\u0000'..'\uFFFD' - special_characters
+       suffix      := '\u0000'..'\uFFFD' - special_characters
+       integer     := min_int | '#' | '#' integer | '#' ',' integer
+       min_int     := '0' | '0' min_int | '0' ',' min_int
+       fraction    := '0'* '#'*
+       exponent    := 'E' '0' '0'*
+     Notation:
+       X*       0 or more instances of X
+       { X }    0 or 1 instances of X
+       X | Y    either X or Y
+       X..Y     any character from X up to Y, inclusive
+       S - T    characters in S, except those in T
+     Single-quote starts a quoted section, to be terminated at the next
+     single-quote or string end.  Double single-quote gives a single
+     single-quote.
+   */
+  bool quoting = false;
+  bool seen_semicolon = false;
+
+  HANDLE_QUOTE;
+  for (;;)
+    {
+      /* Parse prefix.  */
+      while (*format != '\0'
+            && !(!quoting && (*format == '0' || *format == '#')))
+       {
+         if (format[0] == '\\')
+           {
+             if (format[1] == 'u'
+                 && c_isxdigit (format[2])
+                 && c_isxdigit (format[3])
+                 && c_isxdigit (format[4])
+                 && c_isxdigit (format[5]))
+               format += 6;
+             else
+               format += 2;
+           }
+         else
+           format += 1;
+         HANDLE_QUOTE;
+       }
+
+      /* Parse integer.  */
+      if (!(!quoting && (*format == '0' || *format == '#')))
+       return false;
+      while (!quoting && *format == '#')
+       {
+         format++;
+         HANDLE_QUOTE;
+         if (!quoting && *format == ',')
+           {
+             format++;
+             HANDLE_QUOTE;
+           }
+       }
+      while (!quoting && *format == '0')
+       {
+         format++;
+         HANDLE_QUOTE;
+         if (!quoting && *format == ',')
+           {
+             format++;
+             HANDLE_QUOTE;
+           }
+       }
+
+      /* Parse fraction.  */
+      if (!quoting && *format == '.')
+       {
+         format++;
+         HANDLE_QUOTE;
+         while (!quoting && *format == '0')
+           {
+             format++;
+             HANDLE_QUOTE;
+           }
+         while (!quoting && *format == '#')
+           {
+             format++;
+             HANDLE_QUOTE;
+           }
+       }
+
+      /* Parse exponent.  */
+      if (!quoting && *format == 'E')
+       {
+         const char *format_save = format;
+         format++;
+         HANDLE_QUOTE;
+         if (!quoting && *format == '0')
+           {
+             do
+               {
+                 format++;
+                 HANDLE_QUOTE;
+               }
+             while (!quoting && *format == '0');
+           }
+         else
+           {
+             /* Back up.  */
+             format = format_save;
+             quoting = false;
+           }
+       }
+
+      /* Parse suffix.  */
+      while (*format != '\0'
+            && (seen_semicolon || !(!quoting && *format == ';')))
+       {
+         if (format[0] == '\\')
+           {
+             if (format[1] == 'u'
+                 && c_isxdigit (format[2])
+                 && c_isxdigit (format[3])
+                 && c_isxdigit (format[4])
+                 && c_isxdigit (format[5]))
+               format += 6;
+             else
+               format += 2;
+           }
+         else
+           format += 1;
+         HANDLE_QUOTE;
+       }
+
+      if (seen_semicolon || !(!quoting && *format == ';'))
+       break;
+    }
+
+  return (*format == '\0');
+}
+
+/* Return true if a format is a valid choiceFormatPattern.
+   Extracts argument type information into spec.  */
+static bool
+choice_format_parse (format, spec)
+     const char *format;
+     struct spec *spec;
+{
+  /* Pattern syntax:
+       pattern   := | choice | choice '|' pattern
+       choice    := number separator messageformat
+       separator := '<' | '#' | '\u2264'
+     Single-quote starts a quoted section, to be terminated at the next
+     single-quote or string end.  Double single-quote gives a single
+     single-quote.
+   */
+  bool quoting = false;
+
+  HANDLE_QUOTE;
+  if (*format == '\0')
+    return true;
+  for (;;)
+    {
+      /* Don't bother looking too precisely into the syntax of the number.
+        It can contain various Unicode characters.  */
+      char *msgformat;
+      char *mp;
+
+      /* Parse number.  */
+      while (*format != '\0'
+            && !(!quoting && (*format == '<' || *format == '#'
+                              || strncmp (format, "\\u2264", 6) == 0
+                              || *format == '|')))
+       {
+         if (format[0] == '\\')
+           {
+             if (format[1] == 'u'
+                 && c_isxdigit (format[2])
+                 && c_isxdigit (format[3])
+                 && c_isxdigit (format[4])
+                 && c_isxdigit (format[5]))
+               format += 6;
+             else
+               format += 2;
+           }
+         else
+           format += 1;
+         HANDLE_QUOTE;
+       }
+
+      /* Short clause at end of pattern is valid and is ignored!  */
+      if (*format == '\0')
+       break;
+
+      if (*format == '<' || *format == '#')
+       format += 1;
+      else if (strncmp (format, "\\u2264", 6) == 0)
+       format += 6;
+      else
+       return false;
+      HANDLE_QUOTE;
+
+      msgformat = (char *) alloca (strlen (format) + 1);
+      mp = msgformat;
+
+      while (*format != '\0' && !(!quoting && *format == '|'))
+       {
+         *mp++ = *format++;
+         HANDLE_QUOTE;
+       }
+      *mp = '\0';
+
+      if (!message_format_parse (msgformat, spec))
+       return false;
+
+      if (*format == '\0')
+       break;
+
+      format++;
+      HANDLE_QUOTE;
+    }
+
+  return true;
+}
+
+static int
+numbered_arg_compare (p1, p2)
+     const void *p1;
+     const void *p2;
+{
+  unsigned int n1 = ((const struct numbered_arg *) p1)->number;
+  unsigned int n2 = ((const struct numbered_arg *) p2)->number;
+
+  return (n1 > n2 ? 1 : n1 < n2 ? -1 : 0);
+}
+
+static void *
+format_parse (format)
+     const char *format;
+{
+  struct spec spec;
+  struct spec *result;
+
+  spec.directives = 0;
+  spec.numbered_arg_count = 0;
+  spec.allocated = 0;
+  spec.numbered = NULL;
+
+  if (!message_format_parse (format, &spec))
+    goto bad_format;
+
+  /* Sort the numbered argument array, and eliminate duplicates.  */
+  if (spec.numbered_arg_count > 1)
+    {
+      unsigned int i, j;
+      bool err;
+
+      qsort (spec.numbered, spec.numbered_arg_count,
+            sizeof (struct numbered_arg), numbered_arg_compare);
+
+      /* Remove duplicates: Copy from i to j, keeping 0 <= j <= i.  */
+      err = false;
+      for (i = j = 0; i < spec.numbered_arg_count; i++)
+       if (j > 0 && spec.numbered[i].number == spec.numbered[j-1].number)
+         {
+           enum format_arg_type type1 = spec.numbered[i].type;
+           enum format_arg_type type2 = spec.numbered[j-1].type;
+           enum format_arg_type type_both;
+
+           if (type1 == type2 || type2 == FAT_OBJECT)
+             type_both = type1;
+           else if (type1 == FAT_OBJECT)
+             type_both = type2;
+           else
+             /* Incompatible types.  */
+             type_both = FAT_NONE, err = true;
+
+           spec.numbered[j-1].type = type_both;
+         }
+       else
+         {
+           if (j < i)
+             {
+               spec.numbered[j].number = spec.numbered[i].number;
+               spec.numbered[j].type = spec.numbered[i].type;
+             }
+           j++;
+         }
+      spec.numbered_arg_count = j;
+      if (err)
+       goto bad_format;
+    }
+
+  result = (struct spec *) xmalloc (sizeof (struct spec));
+  *result = spec;
+  return result;
+
+ bad_format:
+  if (spec.numbered != NULL)
+    free (spec.numbered);
+  return NULL;
+}
+
+static void
+format_free (descr)
+     void *descr;
+{
+  struct spec *spec = (struct spec *) descr;
+
+  if (spec->numbered != NULL)
+    free (spec->numbered);
+  free (spec);
+}
+
+static int
+format_get_number_of_directives (descr)
+     void *descr;
+{
+  struct spec *spec = (struct spec *) descr;
+
+  return spec->directives;
+}
+
+static bool
+format_check (pos, msgid_descr, msgstr_descr)
+     const lex_pos_ty *pos;
+     void *msgid_descr;
+     void *msgstr_descr;
+{
+  struct spec *spec1 = (struct spec *) msgid_descr;
+  struct spec *spec2 = (struct spec *) msgstr_descr;
+  bool err = false;
+
+  if (spec1->numbered_arg_count + spec2->numbered_arg_count > 0)
+    {
+      unsigned int i;
+      unsigned int n = MAX (spec1->numbered_arg_count, spec2->numbered_arg_count);
+
+      /* Check the argument names are the same.
+        Both arrays are sorted.  We search for the first difference.  */
+      for (i = 0; i < n; i++)
+       {
+         int cmp = (i >= spec1->numbered_arg_count ? 1 :
+                    i >= spec2->numbered_arg_count ? -1 :
+                    spec1->numbered[i].number > spec2->numbered[i].number ? 1 :
+                    spec1->numbered[i].number < spec2->numbered[i].number ? -1 :
+                    0);
+
+         if (cmp > 0)
+           {
+             error_with_progname = false;
+             error_at_line (0, 0, pos->file_name, pos->line_number,
+                            _("a format specification for argument {%u} doesn't exist in 'msgid'"),
+                            spec2->numbered[i].number);
+             error_with_progname = true;
+             err = true;
+             break;
+           }
+         else if (cmp < 0)
+           {
+             error_with_progname = false;
+             error_at_line (0, 0, pos->file_name, pos->line_number,
+                            _("a format specification for argument {%u} doesn't exist in 'msgstr'"),
+                            spec1->numbered[i].number);
+             error_with_progname = true;
+             err = true;
+             break;
+           }
+       }
+      /* Check the argument types are the same.  */
+      if (!err)
+       for (i = 0; i < spec2->numbered_arg_count; i++)
+         if (spec1->numbered[i].type != spec2->numbered[i].type)
+           {
+             error_with_progname = false;
+             error_at_line (0, 0, pos->file_name, pos->line_number,
+                            _("format specifications in 'msgid' and 'msgstr' for argument {%u} are not the same"),
+                            spec2->numbered[i].number);
+             error_with_progname = true;
+             err = true;
+             break;
+           }
+    }
+
+  return err;
+}
+
+
+struct formatstring_parser formatstring_java =
+{
+  format_parse,
+  format_free,
+  format_get_number_of_directives,
+  format_check
+};
+
+
+#ifdef TEST
+
+/* Test program: Print the argument list specification returned by
+   format_parse for strings read from standard input.  */
+
+#include <stdio.h>
+#include "getline.h"
+
+static void
+format_print (descr)
+     void *descr;
+{
+  struct spec *spec = (struct spec *) descr;
+  unsigned int last;
+  unsigned int i;
+
+  if (spec == NULL)
+    {
+      printf ("INVALID");
+      return;
+    }
+
+  printf ("(");
+  last = 0;
+  for (i = 0; i < spec->numbered_arg_count; i++)
+    {
+      unsigned int number = spec->numbered[i].number;
+
+      if (i > 0)
+       printf (" ");
+      if (number < last)
+       abort ();
+      for (; last < number; last++)
+       printf ("_ ");
+      switch (spec->numbered[i].type)
+       {
+       case FAT_OBJECT:
+         printf ("*");
+         break;
+       case FAT_NUMBER:
+         printf ("Number");
+         break;
+       case FAT_DATE:
+         printf ("Date");
+         break;
+       default:
+         abort ();
+       }
+      last = number + 1;
+    }
+  printf (")");
+}
+
+int
+main ()
+{
+  for (;;)
+    {
+      char *line = NULL;
+      size_t line_len = 0;
+      void *descr;
+
+      if (getline (&line, &line_len, stdin) < 0)
+       break;
+
+      descr = format_parse (line);
+
+      format_print (descr);
+      printf ("\n");
+
+      free (line);
+    }
+
+  return 0;
+}
+
+/*
+ * For Emacs M-x compile
+ * Local Variables:
+ * compile-command: "gcc -O -g -Wall -I.. -I../lib -I../intl -DHAVE_CONFIG_H -DTEST format-java.c ../lib/libnlsut.a"
+ * End:
+ */
+
+#endif /* TEST */
diff --git a/tests/format-java-1 b/tests/format-java-1
new file mode 100755 (executable)
index 0000000..6f2e18c
--- /dev/null
@@ -0,0 +1,158 @@
+#! /bin/sh
+
+# Test recognition of Java format strings.
+
+tmpfiles=""
+trap 'rm -fr $tmpfiles' 1 2 3 15
+
+tmpfiles="$tmpfiles f-j-1.data"
+cat <<\EOF > f-j-1.data
+# Valid: one argument
+"abc{0}def"
+# Valid: ten arguments
+"abc{9}def"
+# Valid: two-digit argument numbers
+"abc{00}def"
+# Valid: huge argument numbers
+"abc{500000000}def"
+# Invalid: unterminated
+"abc{"
+# Invalid: unterminated
+"abc{0"
+# Invalid: missing number
+"abc{}def"
+# Invalid: non-digit
+"abc{number}def"
+# Invalid: non-digit
+"abc{-0}def"
+# Valid: two arguments
+"abc{1}def{0}"
+# Valid: multiple uses of same argument
+"abc{1}def{0}ghi{1}"
+# Invalid: broken elementFormat
+"abc{0,}def"
+# Invalid: invalid elementFormat
+"abc{1,string}def"
+# Valid: elementFormat of length 1
+"abc{1,number}def"
+# Valid: elementFormat of length 1
+"abc{1,date}def"
+# Valid: elementFormat of length 1
+"abc{1,time}def"
+# Valid: elementFormat of length 1
+"abc{1,choice}def"
+# Invalid: broken elementFormat
+"abc{1,number,}def"
+# Valid: builtin numberStyle
+"abc{1,number,currency}def"
+# Valid: builtin numberStyle
+"abc{1,number,percent}def"
+# Valid: builtin numberStyle
+"abc{1,number,integer}def"
+# Valid: builtin datetimeStyle
+"abc{1,date,short}def"
+# Valid: builtin datetimeStyle
+"abc{1,date,medium}def"
+# Valid: builtin datetimeStyle
+"abc{1,date,long}def"
+# Valid: builtin datetimeStyle
+"abc{1,date,full}def"
+# Valid: builtin datetimeStyle
+"abc{1,time,short}def"
+# Valid: builtin datetimeStyle
+"abc{1,time,medium}def"
+# Valid: builtin datetimeStyle
+"abc{1,time,long}def"
+# Valid: builtin datetimeStyle
+"abc{1,time,full}def"
+# Valid: dateFormatPattern
+"abc{1,date,foobar}"
+# Valid: dateFormatPattern
+"abc{1,time,foobar}"
+# Valid: dateFormatPattern with comma
+"abc{1,date,foo,bar}"
+# Valid: numberFormatPattern
+"abc{1,number,###,##0}def"
+# Invalid: numberFormatPattern
+"abc{1,number,foobar}"
+# Valid: empty choiceFormatPattern
+"abc{1,choice,}def"
+# Valid: choiceFormatPattern
+"abc{1,choice,0#zero|1#one|2#many}def"
+# Invalid: empty clause in choiceFormatPattern
+"abc{1,choice,|0#zero|1#one|2#many}def"
+# Valid: empty clause at end of choiceFormatPattern
+"abc{1,choice,0#zero|1#one|2#many|}def"
+# Invalid: short clause in choiceFormatPattern
+"abc{1,choice,-1|0#zero|1#one|2#many}def"
+# Valid: short clause at end of choiceFormatPattern
+"abc{1,choice,0#zero|1#one|2#many|3}def"
+# Valid: choiceFormatPattern with different argument
+"abc{1,choice,1#one|2#{0,date}}def"
+# Valid: choiceFormatPattern with same argument
+"abc{1,choice,1#one|2#{1}}def"
+# Valid: choiceFormatPattern with same argument
+"abc{1,choice,1#one|2#{1,number}}def"
+# Invalid: choiceFormatPattern with same argument, type conflict
+"abc{1,choice,1#one|2#{1,date}}def"
+# Invalid: missing opening brace
+"abc1}def{0}"
+# Valid: quoted brace
+"abc1'}'def{0}"
+# Invalid: quoted brace
+"abc{1'}'def"
+# Valid: unterminated quote
+"abc{0}1'}"
+# Valid: quoted brace, '' counts as a single quote
+"abc''1'}'def{0}"
+# Invalid: '' counts as a single quote
+"abc{1''}def"
+# Valid: quote inside elementFormat is hidden
+"abc{1,date,x'y}def"
+# Valid: numberFormatPattern with quote
+"abc{1,number,#0';'}def"
+# Invalid: numberFormatPattern with wrong number syntax
+"abc{1,number,#0;}def"
+# Valid: numberFormatPattern with quote
+"abc{1,number,0.##'E}def"
+# Valid: numberFormatPattern without quote
+"abc{1,number,0.##E}def"
+EOF
+
+: ${XGETTEXT=xgettext}
+n=0
+while read comment; do
+  read string
+  n=`expr $n + 1`
+  tmpfiles="$tmpfiles f-j-1-$n.in f-j-1-$n.po"
+  cat <<EOF > f-j-1-$n.in
+gettext(${string});
+EOF
+  ${XGETTEXT} -L Java -o f-j-1-$n.po f-j-1-$n.in || exit 1
+  test -f f-j-1-$n.po || exit 1
+  fail=
+  if echo "$comment" | grep 'Valid:' > /dev/null; then
+    if grep java-format f-j-1-$n.po > /dev/null; then
+      :
+    else
+      fail=yes
+    fi
+  else
+    if grep java-format f-j-1-$n.po > /dev/null; then
+      fail=yes
+    else
+      :
+    fi
+  fi
+  if test -n "$fail"; then
+    echo "Format string recognition error:" 1>&2
+    cat f-j-1-$n.in 1>&2
+    echo "Got:" 1>&2
+    cat f-j-1-$n.po 1>&2
+    exit 1
+  fi
+done < f-j-1.data
+
+rm -fr $tmpfiles
+
+exit 0
diff --git a/tests/format-java-2 b/tests/format-java-2
new file mode 100755 (executable)
index 0000000..3248321
--- /dev/null
@@ -0,0 +1,147 @@
+#! /bin/sh
+
+# Test checking of Java format strings.
+
+tmpfiles=""
+trap 'rm -fr $tmpfiles' 1 2 3 15
+
+tmpfiles="$tmpfiles f-j-2.data"
+cat <<\EOF > f-j-2.data
+# Invalid: invalid msgstr
+msgid  "abc{0}def"
+msgstr "abc{"
+# Valid: same arguments
+msgid  "abc{1}def"
+msgstr "xyz{1}"
+# Valid: same arguments, differently written
+msgid  "abc{1}def"
+msgstr "xyz{01}"
+# Valid: permutation
+msgid  "abc{2}{0}{1}def"
+msgstr "xyz{1}{0}{2}"
+# Invalid: too few arguments
+msgid  "abc{1}def{0}"
+msgstr "xyz{0}"
+# Invalid: too many arguments
+msgid  "abc{0}def"
+msgstr "xyz{0}uvw{1}"
+# Invalid: missing non-final argument
+msgid  "abc{1}def{0}"
+msgstr "xyz{1}"
+# Invalid: added non-final argument
+msgid  "abc{1}def"
+msgstr "xyz{0}{1}"
+# Invalid: different number of arguments
+msgid  "abc{500000000}def"
+msgstr "xyz{500000001}"
+# Valid: type compatibility
+msgid  "abc{1,number}"
+msgstr "xyz{1,choice,0#zero|1#{1,number}}"
+# Valid: type compatibility
+msgid  "abc{1,number}"
+msgstr "xyz{1,number,currency}"
+# Valid: type compatibility
+msgid  "abc{1,number}"
+msgstr "xyz{1,number,percent}"
+# Valid: type compatibility
+msgid  "abc{1,number}"
+msgstr "xyz{1,number,integer}"
+# Valid: type compatibility
+msgid  "abc{1,number}"
+msgstr "xyz{1,number,###,##0}"
+# Valid: type compatibility
+msgid  "abc{1,date}"
+msgstr "xyz{1,time}"
+# Valid: type compatibility
+msgid  "abc{1,date}"
+msgstr "xyz{1,date,short}"
+# Valid: type compatibility
+msgid  "abc{1,date}"
+msgstr "xyz{1,date,medium}"
+# Valid: type compatibility
+msgid  "abc{1,date}"
+msgstr "xyz{1,date,long}"
+# Valid: type compatibility
+msgid  "abc{1,date}"
+msgstr "xyz{1,date,full}"
+# Valid: type compatibility
+msgid  "abc{1,date}"
+msgstr "xyz{1,date,yyyy-MM-dd}"
+# Valid: type compatibility
+msgid  "abc{1,time}"
+msgstr "xyz{1,time,short}"
+# Valid: type compatibility
+msgid  "abc{1,time}"
+msgstr "xyz{1,time,medium}"
+# Valid: type compatibility
+msgid  "abc{1,time}"
+msgstr "xyz{1,time,long}"
+# Valid: type compatibility
+msgid  "abc{1,time}"
+msgstr "xyz{1,time,full}"
+# Valid: type compatibility
+msgid  "abc{1,time}"
+msgstr "xyz{1,time,}"
+# Valid: type compatibility
+msgid  "abc{1,time}"
+msgstr "xyz{1,time,hh:mm:ss}"
+# Invalid: type incompatibility
+msgid  "abc{1}"
+msgstr "xyz{1,number}"
+# Invalid: type incompatibility
+msgid  "abc{1}"
+msgstr "xyz{1,date}"
+# Invalid: type incompatibility
+msgid  "abc{1,time}"
+msgstr "xyz{1,number}"
+# Invalid: type incompatibility
+msgid  "abc{1,number}"
+msgstr "xyz{1,date}"
+# Invalid: type incompatibility
+msgid  "abc{1}"
+msgstr "xyz{1,choice,0#zero|1#{1,number}}"
+# Invalid: type incompatibility
+msgid  "abc{1}"
+msgstr "xyz{1,choice,0#zero|1#{0,number}}"
+# Invalid: type incompatibility
+msgid  "abc{0,number}{1}"
+msgstr "xyz{0,choice,0#zero|1#{1,number}}"
+EOF
+
+: ${MSGFMT=msgfmt}
+n=0
+while read comment; do
+  read msgid_line
+  read msgstr_line
+  n=`expr $n + 1`
+  tmpfiles="$tmpfiles f-j-2-$n.po f-j-2-$n.mo"
+  cat <<EOF > f-j-2-$n.po
+#, java-format
+${msgid_line}
+${msgstr_line}
+EOF
+  fail=
+  if echo "$comment" | grep 'Valid:' > /dev/null; then
+    if ${MSGFMT} -c -o f-j-2-$n.mo f-j-2-$n.po; then
+      :
+    else
+      fail=yes
+    fi
+  else
+    ${MSGFMT} -c -o f-j-2-$n.mo f-j-2-$n.po 2> /dev/null
+    if test $? = 1; then
+      :
+    else
+      fail=yes
+    fi
+  fi
+  if test -n "$fail"; then
+    echo "Format string checking error:" 1>&2
+    cat f-j-2-$n.po 1>&2
+    exit 1
+  fi
+done < f-j-2.data
+
+rm -fr $tmpfiles
+
+exit 0