summaryrefslogtreecommitdiff
path: root/Master/texmf-dist/source/support/texosquery/java
diff options
context:
space:
mode:
Diffstat (limited to 'Master/texmf-dist/source/support/texosquery/java')
-rw-r--r--Master/texmf-dist/source/support/texosquery/java/FileListType.java24
-rw-r--r--Master/texmf-dist/source/support/texosquery/java/FilePathSortComparator.java186
-rw-r--r--Master/texmf-dist/source/support/texosquery/java/FileSortComparator.java170
-rw-r--r--Master/texmf-dist/source/support/texosquery/java/FileSortType.java131
-rw-r--r--Master/texmf-dist/source/support/texosquery/java/FileWalkVisitor.java221
-rw-r--r--Master/texmf-dist/source/support/texosquery/java/Manifest-jre5.txt2
-rw-r--r--Master/texmf-dist/source/support/texosquery/java/Manifest-jre7.txt2
-rw-r--r--Master/texmf-dist/source/support/texosquery/java/Manifest-jre8.txt2
-rw-r--r--Master/texmf-dist/source/support/texosquery/java/QueryAction.java448
-rw-r--r--Master/texmf-dist/source/support/texosquery/java/QueryActionType.java19
-rw-r--r--Master/texmf-dist/source/support/texosquery/java/TeXOSQuery.java4459
-rw-r--r--Master/texmf-dist/source/support/texosquery/java/TeXOSQueryJRE5.java54
-rw-r--r--Master/texmf-dist/source/support/texosquery/java/TeXOSQueryJRE7.java197
-rw-r--r--Master/texmf-dist/source/support/texosquery/java/TeXOSQueryJRE8.java377
14 files changed, 6292 insertions, 0 deletions
diff --git a/Master/texmf-dist/source/support/texosquery/java/FileListType.java b/Master/texmf-dist/source/support/texosquery/java/FileListType.java
new file mode 100644
index 00000000000..46d4732fc4d
--- /dev/null
+++ b/Master/texmf-dist/source/support/texosquery/java/FileListType.java
@@ -0,0 +1,24 @@
+/*
+ Copyright (C) 2016 Nicola L.C. Talbot
+ www.dickimaw-books.com
+
+ This work may be distributed and/or modified under the
+ conditions of the LaTeX Project Public License, either version 1.3
+ of this license or (at your option) any later version.
+ The latest version of this license is in
+ http://www.latex-project.org/lppl.txt
+ and version 1.3 or later is part of all distributions of LaTeX
+ version 2005/12/01 or later.
+*/
+package com.dickimawbooks.texosquery;
+
+/**
+ * Indicates the type of file to be included in listings.
+ * @since 1.2
+ */
+public enum FileListType
+{
+ FILE_LIST_ANY, FILE_LIST_DIRECTORIES_ONLY,
+ FILE_LIST_REGULAR_FILES_ONLY
+}
+
diff --git a/Master/texmf-dist/source/support/texosquery/java/FilePathSortComparator.java b/Master/texmf-dist/source/support/texosquery/java/FilePathSortComparator.java
new file mode 100644
index 00000000000..436a23867b9
--- /dev/null
+++ b/Master/texmf-dist/source/support/texosquery/java/FilePathSortComparator.java
@@ -0,0 +1,186 @@
+/*
+ Copyright (C) 2016 Nicola L.C. Talbot
+ www.dickimaw-books.com
+
+ This work may be distributed and/or modified under the
+ conditions of the LaTeX Project Public License, either version 1.3
+ of this license or (at your option) any later version.
+ The latest version of this license is in
+ http://www.latex-project.org/lppl.txt
+ and version 1.3 or later is part of all distributions of LaTeX
+ version 2005/12/01 or later.
+*/
+package com.dickimawbooks.texosquery;
+
+import java.util.Comparator;
+import java.nio.file.Path;
+import java.nio.file.Files;
+import java.nio.file.attribute.FileTime;
+
+/**
+ * Used to compare two paths according to the given sort type.
+ * This uses the java.nio.file library, which was introduced in Java 7,
+ * so this isn't available for the JRE5 version.
+ * @since 1.2
+ */
+public class FilePathSortComparator implements Comparator<Path>
+{
+ /**
+ * Creates a new comparator for ordering path listings.
+ * @param sortType the way in which the files should be ordered
+ */
+ public FilePathSortComparator(FileSortType sortType)
+ {
+ this.sortType = sortType;
+ }
+
+ /**
+ * Compares two paths according to this object's sort type.
+ * @param path1 the first path
+ * @param path2 the second path
+ * @return -1 if path1 is considered less than path2, 0 if both are the same
+ * or +1 if path2 is greater than path1, according to the sort
+ * criteria
+ */
+ @Override
+ public int compare(Path path1, Path path2)
+ {
+ int idx=-1;
+ int result=0;
+ long date1=0L;
+ long date2=0L;
+ long size1=0L;
+ long size2=0L;
+ String ext1="";
+ String ext2="";
+ FileTime time1=null;
+ FileTime time2=null;
+
+ String name1 = path1.toString();
+ String name2 = path2.toString();
+
+ String basename1 = path1.getName(path1.getNameCount()-1).toString();
+ String basename2 = path2.getName(path2.getNameCount()-1).toString();
+
+ switch (sortType)
+ {
+ case FILE_SORT_NAME_ASCENDING:
+ return name1.compareTo(name2);
+ case FILE_SORT_NAME_DESCENDING:
+ return name2.compareTo(name1);
+ case FILE_SORT_NAME_NOCASE_ASCENDING:
+ return name1.compareToIgnoreCase(name2);
+ case FILE_SORT_NAME_NOCASE_DESCENDING:
+ return name2.compareToIgnoreCase(name1);
+ case FILE_SORT_EXT_ASCENDING:
+
+ idx = basename1.lastIndexOf(".");
+
+ ext1 = idx > -1 ? basename1.substring(idx+1) : "";
+
+ idx = basename2.lastIndexOf(".");
+
+ ext2 = idx > -1 ? basename2.substring(idx+1) : "";
+
+ result = ext1.compareTo(ext2);
+
+ // If the extensions are the same, compare paths instead
+
+ return result == 0 ? name1.compareTo(name2) : result;
+
+ case FILE_SORT_EXT_DESCENDING:
+
+ idx = basename1.lastIndexOf(".");
+
+ ext1 = idx > -1 ? basename1.substring(idx+1) : "";
+
+ idx = basename2.lastIndexOf(".");
+
+ ext2 = idx > -1 ? basename2.substring(idx+1) : "";
+
+ result = ext2.compareTo(ext1);
+
+ return result == 0 ? name2.compareTo(name1) : result;
+
+ case FILE_SORT_DATE_ASCENDING:
+
+ try
+ {
+ time1 = Files.getLastModifiedTime(path1);
+ }
+ catch (Exception e)
+ {
+ // IO error or security manager has prohibited access
+ return 1;
+ }
+
+ try
+ {
+ time2 = Files.getLastModifiedTime(path2);
+ }
+ catch (Exception e)
+ {
+ // IO error or security manager has prohibited access
+ return -1;
+ }
+
+ return time1.compareTo(time2);
+
+ case FILE_SORT_DATE_DESCENDING:
+
+ try
+ {
+ time1 = Files.getLastModifiedTime(path1);
+ }
+ catch (Exception e)
+ {
+ // IO error or security manager has prohibited access
+ return -1;
+ }
+
+ try
+ {
+ time2 = Files.getLastModifiedTime(path2);
+ }
+ catch (Exception e)
+ {
+ // IO error or security manager has prohibited access
+ return 1;
+ }
+
+ return time2.compareTo(time1);
+
+ case FILE_SORT_SIZE_ASCENDING:
+
+ try
+ {
+ size1 = Files.size(path1);
+ size2 = Files.size(path2);
+ }
+ catch (Exception e)
+ {// file missing or no read access or for some other
+ // reason the file size can't be obtained.
+ }
+
+ return size1 == size2 ? 0 : (size1 < size2 ? -1 : 0);
+
+ case FILE_SORT_SIZE_DESCENDING:
+
+ try
+ {
+ size1 = Files.size(path1);
+ size2 = Files.size(path2);
+ }
+ catch (Exception e)
+ {// file missing or no read access or for some other
+ // reason the file size can't be obtained.
+ }
+
+ return size1 == size2 ? 0 : (size1 > size2 ? -1 : 0);
+ }
+
+ return 0;
+ }
+
+ private FileSortType sortType;
+}
diff --git a/Master/texmf-dist/source/support/texosquery/java/FileSortComparator.java b/Master/texmf-dist/source/support/texosquery/java/FileSortComparator.java
new file mode 100644
index 00000000000..d382e07f305
--- /dev/null
+++ b/Master/texmf-dist/source/support/texosquery/java/FileSortComparator.java
@@ -0,0 +1,170 @@
+/*
+ Copyright (C) 2016 Nicola L.C. Talbot
+ www.dickimaw-books.com
+
+ This work may be distributed and/or modified under the
+ conditions of the LaTeX Project Public License, either version 1.3
+ of this license or (at your option) any later version.
+ The latest version of this license is in
+ http://www.latex-project.org/lppl.txt
+ and version 1.3 or later is part of all distributions of LaTeX
+ version 2005/12/01 or later.
+*/
+package com.dickimawbooks.texosquery;
+
+import java.util.Comparator;
+import java.io.File;
+
+/**
+ * Used to compare two file names according to the given sort type.
+ * @since 1.2
+ */
+public class FileSortComparator implements Comparator<String>
+{
+ /**
+ * Creates a new comparator for ordering file listings.
+ * @param baseDir the directory containing the listed files
+ * @param sortType the way in which the files should be ordered
+ */
+ public FileSortComparator(File baseDir, FileSortType sortType)
+ {
+ this.baseDir = baseDir;
+ this.sortType = sortType;
+ }
+
+ /**
+ * Compares two files according to this object's sort type.
+ * @param name1 the name of the first file
+ * @param name2 the name of the second file
+ * @return -1 if name1 is considered less than name2, 0 if both are the same
+ * or +1 if name2 is greater than name1, according to the sort
+ * criteria
+ */
+ @Override
+ public int compare(String name1, String name2)
+ {
+ int idx=-1;
+ int result=0;
+ long date1=0L;
+ long date2=0L;
+ long size1=0L;
+ long size2=0L;
+ String ext1="";
+ String ext2="";
+ File file1;
+ File file2;
+
+ switch (sortType)
+ {
+ case FILE_SORT_NAME_ASCENDING:
+ return name1.compareTo(name2);
+ case FILE_SORT_NAME_DESCENDING:
+ return name2.compareTo(name1);
+ case FILE_SORT_NAME_NOCASE_ASCENDING:
+ return name1.compareToIgnoreCase(name2);
+ case FILE_SORT_NAME_NOCASE_DESCENDING:
+ return name2.compareToIgnoreCase(name1);
+ case FILE_SORT_EXT_ASCENDING:
+
+ idx = name1.lastIndexOf(".");
+
+ ext1 = idx > -1 ? name1.substring(idx+1) : "";
+
+ idx = name2.lastIndexOf(".");
+
+ ext2 = idx > -1 ? name2.substring(idx+1) : "";
+
+ result = ext1.compareTo(ext2);
+
+ return result == 0 ? name1.compareTo(name2) : result;
+
+ case FILE_SORT_EXT_DESCENDING:
+
+ idx = name1.lastIndexOf(".");
+
+ ext1 = idx > -1 ? name1.substring(idx+1) : "";
+
+ idx = name2.lastIndexOf(".");
+
+ ext2 = idx > -1 ? name2.substring(idx+1) : "";
+
+ result = ext2.compareTo(ext1);
+
+ return result == 0 ? name2.compareTo(name1) : result;
+
+ case FILE_SORT_DATE_ASCENDING:
+
+ file1 = new File(baseDir, name1);
+ file2 = new File(baseDir, name2);
+
+ try
+ {
+ date1 = file1.lastModified();
+ date2 = file2.lastModified();
+ }
+ catch (Exception e)
+ {// file missing or no read access or for some other
+ // reason the last modified date can't be obtained.
+ }
+
+ return date1 == date2 ? 0 : (date1 < date2 ? -1 : 0);
+
+ case FILE_SORT_DATE_DESCENDING:
+
+ file1 = new File(baseDir, name1);
+ file2 = new File(baseDir, name2);
+
+ try
+ {
+ date1 = file1.lastModified();
+ date2 = file2.lastModified();
+ }
+ catch (Exception e)
+ {// file missing or no read access or for some other
+ // reason the last modified date can't be obtained.
+ }
+
+ return date1 == date2 ? 0 : (date1 > date2 ? -1 : 0);
+
+ case FILE_SORT_SIZE_ASCENDING:
+
+ file1 = new File(baseDir, name1);
+ file2 = new File(baseDir, name2);
+
+ try
+ {
+ size1 = file1.length();
+ size2 = file2.length();
+ }
+ catch (Exception e)
+ {// file missing or no read access or for some other
+ // reason the file size can't be obtained.
+ }
+
+ return size1 == size2 ? 0 : (size1 < size2 ? -1 : 0);
+
+ case FILE_SORT_SIZE_DESCENDING:
+
+ file1 = new File(baseDir, name1);
+ file2 = new File(baseDir, name2);
+
+ try
+ {
+ size1 = file1.length();
+ size2 = file2.length();
+ }
+ catch (Exception e)
+ {// file missing or no read access or for some other
+ // reason the file size can't be obtained.
+ }
+
+ return size1 == size2 ? 0 : (size1 > size2 ? -1 : 0);
+ }
+
+ return 0;
+ }
+
+
+ private File baseDir;
+ private FileSortType sortType;
+}
diff --git a/Master/texmf-dist/source/support/texosquery/java/FileSortType.java b/Master/texmf-dist/source/support/texosquery/java/FileSortType.java
new file mode 100644
index 00000000000..4b3e303ff22
--- /dev/null
+++ b/Master/texmf-dist/source/support/texosquery/java/FileSortType.java
@@ -0,0 +1,131 @@
+/*
+ Copyright (C) 2016 Nicola L.C. Talbot
+ www.dickimaw-books.com
+
+ This work may be distributed and/or modified under the
+ conditions of the LaTeX Project Public License, either version 1.3
+ of this license or (at your option) any later version.
+ The latest version of this license is in
+ http://www.latex-project.org/lppl.txt
+ and version 1.3 or later is part of all distributions of LaTeX
+ version 2005/12/01 or later.
+*/
+package com.dickimawbooks.texosquery;
+
+/**
+ * Indicates the ordering of files in listings.
+ * @since 1.2
+ */
+public enum FileSortType
+{
+ FILE_SORT_DEFAULT ("default"),
+ FILE_SORT_DATE_ASCENDING ("date-ascending", "date", "date-asc"),
+ FILE_SORT_DATE_DESCENDING ("date-descending", "date-des"),
+ FILE_SORT_SIZE_ASCENDING ("size-ascending", "size", "size-asc"),
+ FILE_SORT_SIZE_DESCENDING ("size-descending", "size-des"),
+ FILE_SORT_NAME_ASCENDING ("name-ascending", "name", "name-asc"),
+ FILE_SORT_NAME_DESCENDING ("name-descending", "name-des"),
+ FILE_SORT_NAME_NOCASE_ASCENDING ("iname-ascending", "iname", "iname-asc"),
+ FILE_SORT_NAME_NOCASE_DESCENDING ("iname-descending", "iname-des"),
+ FILE_SORT_EXT_ASCENDING ("ext-ascending", "ext", "ext-asc"),
+ FILE_SORT_EXT_DESCENDING ("ext-descending", "ext-des");
+
+ private final String name, altName1, altName2;
+
+ FileSortType(String name)
+ {
+ this(name, null, null);
+ }
+
+ FileSortType(String name, String altName)
+ {
+ this(name, altName, null);
+ }
+
+ FileSortType(String name, String altName1, String altName2)
+ {
+ this.name = name;
+ this.altName1 = altName1;
+ this.altName2 = altName2;
+ }
+
+ /**
+ * Returns available option names.
+ */
+ private String options()
+ {
+ if (altName1 == null && altName2 == null)
+ {
+ return name;
+ }
+
+ if (altName2 == null)
+ {
+ return String.format("%s (or %s)", name, altName1);
+ }
+
+ return String.format("%s (or %s or %s)", name, altName1, altName2);
+ }
+
+ /**
+ * Checks if the given type matches this option.
+ * @return true if type matches this option
+ */
+ private boolean isOption(String type)
+ {
+ return type.equals(name) || type.equals(altName1)
+ || type.equals(altName2);
+ }
+
+ /**
+ * Returns a list of available sort options used in the command
+ * line invocation.
+ * @return list of options for syntax information.
+ */
+ public static String getFileSortOptions()
+ {
+ StringBuilder builder = new StringBuilder();
+
+ for (FileSortType t : FileSortType.values())
+ {
+ if (builder.length() > 0)
+ {
+ builder.append(", ");
+ }
+
+ builder.append(t.options());
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Gets the sort type from the given string.
+ * @param type the sort type as provided by the command line
+ * invocation
+ * @return a FileSortType object that indicates the sort type
+ */
+ public static FileSortType getFileSortType(String type)
+ {
+ if (type == null)
+ {
+ return FileSortType.FILE_SORT_DEFAULT;
+ }
+
+ for (FileSortType t : FileSortType.values())
+ {
+ if (t.isOption(type))
+ {
+ return t;
+ }
+ }
+
+ throw new IllegalArgumentException( "Invalid sort type: "+type);
+ }
+
+ public String toString()
+ {
+ return name;
+ }
+}
+
diff --git a/Master/texmf-dist/source/support/texosquery/java/FileWalkVisitor.java b/Master/texmf-dist/source/support/texosquery/java/FileWalkVisitor.java
new file mode 100644
index 00000000000..89e4fe73815
--- /dev/null
+++ b/Master/texmf-dist/source/support/texosquery/java/FileWalkVisitor.java
@@ -0,0 +1,221 @@
+/*
+ Copyright (C) 2016 Nicola L.C. Talbot
+ www.dickimaw-books.com
+
+ This work may be distributed and/or modified under the
+ conditions of the LaTeX Project Public License, either version 1.3
+ of this license or (at your option) any later version.
+ The latest version of this license is in
+ http://www.latex-project.org/lppl.txt
+ and version 1.3 or later is part of all distributions of LaTeX
+ version 2005/12/01 or later.
+*/
+package com.dickimawbooks.texosquery;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.FileNotFoundException;
+import java.nio.file.Path;
+import java.nio.file.Files;
+import java.nio.file.FileVisitResult;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Vector;
+import java.util.Comparator;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+/**
+ * This class is used during a file walk to determine if a directory
+ * may be descended and to add allowed files to the list.
+ * This relies on the java.nio.file library, which was introduced to
+ * Java 7, so this isn't available for the JRE5 version.
+ * @since 1.2
+ */
+public class FileWalkVisitor extends SimpleFileVisitor<Path>
+{
+ /**
+ * Creates a new visitor for a file walk. This will only allow
+ * regular, non-hidden, readable files, where the name matches
+ * the supplied regular expression. The found files are inserted
+ * into the results list according to the comparator.
+ */
+ public FileWalkVisitor(TeXOSQuery invoker, String regex,
+ Comparator<Path> comparator)
+ {
+ this.invoker = invoker;
+ this.comparator = comparator;
+ pattern = Pattern.compile(regex);
+ result = new Vector<Path>();
+ }
+
+ /**
+ * Returns the list built during the file walk.
+ * @return file list
+ */
+ public Vector<Path> getList()
+ {
+ return result;
+ }
+
+ /**
+ * Invoked for a directory before entries in the directory are
+ * visited.
+ *
+ * Hidden directories are automatically skipped, even if
+ * openin_any is <tt>a</tt>. Symbolic links and directories without
+ * read access are also skipped.
+ *
+ * @param dir the directory about to be visited
+ * @param attrs the directory's basic attributes
+ * @return the visit result
+ */
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
+ throws IOException
+ {
+ if (Files.isHidden(dir) || !Files.isReadable(dir)
+ || Files.isSymbolicLink(dir))
+ {
+ invoker.info(String.format(
+ "Walk skipping directory: %s", dir.toString()));
+ return FileVisitResult.SKIP_SUBTREE;
+ }
+
+ // We don't need to check openin_any here as the starting
+ // directory of the walk should already have been checked to
+ // ensure it's on the CWD path.
+
+ return FileVisitResult.CONTINUE;
+ }
+
+
+ /**
+ * Invoked for a file in a directory.
+ *
+ * Hidden files are automatically skipped, regardless of
+ * openin_any. Files that don't have read access are also skipped
+ * and so are symbolic files.
+ *
+ * @param file a reference to the file
+ * @param attrs the file's basic attributes
+ * @return the visit result
+ */
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+ throws IOException
+ {
+ if (Files.isHidden(file) || !Files.isReadable(file)
+ || attrs.isSymbolicLink())
+ {
+ invoker.info(String.format(
+ "Walk skipping file: %s", file.toString()));
+ return FileVisitResult.CONTINUE;
+ }
+
+ // Does the file name match the supplied pattern?
+
+ String name = file.getName(file.getNameCount()-1).toString();
+
+ Matcher m = pattern.matcher(name);
+
+ if (m.matches())
+ {
+ addPath(file);
+ }
+
+ return FileVisitResult.CONTINUE;
+ }
+
+ /**
+ * Inserts path into results list according to the comparator.
+ * @param path the path to be added to the list
+ */
+ private void addPath(Path path)
+ {
+ int n = result.size();
+
+ if (n == 0)
+ {
+ result.add(path);
+ return;
+ }
+
+ for (int i = 0; i < n; i++)
+ {
+ Path thisPath = result.get(i);
+
+ if (comparator.compare(thisPath, path) > 0)
+ {
+ result.add(i, path);
+ return;
+ }
+ }
+
+ result.add(path);
+ }
+
+ /**
+ *Walks the path starting from the given directory, which must be the
+ *current working directory or a descendent.
+ */
+ public static String walk(TeXOSQuery invoker,
+ String separator,
+ String regex,
+ File directory,
+ FileSortType sortType)
+ throws IOException
+ {
+ if (!directory.exists())
+ {
+ throw new FileNotFoundException(
+ String.format("No such file: %s", directory.toString()));
+ }
+
+ if (!directory.isDirectory())
+ {
+ throw new IOException(
+ String.format("Not a directory: %s", directory.toString()));
+ }
+
+ directory = directory.getCanonicalFile();
+
+ File cwd = new File(invoker.getSystemProperty("user.dir", "."));
+
+ if (!cwd.equals(directory) && !invoker.isFileInTree(directory, cwd))
+ {
+ throw new IOException(
+ String.format("Walk must start in current working directory path: %s",
+ cwd.toString()));
+ }
+
+ Path start = directory.toPath();
+
+ FileWalkVisitor visitor = new FileWalkVisitor(invoker, regex,
+ new FilePathSortComparator(sortType));
+
+ Files.walkFileTree(start, visitor);
+
+ Vector<Path> result = visitor.getList();
+
+ StringBuilder builder = new StringBuilder();
+
+ for (Path path : result)
+ {
+ if (builder.length() > 0)
+ {
+ builder.append(separator);
+ }
+
+ builder.append(
+ invoker.escapeFileName(start.relativize(path).toString()));
+ }
+
+ return builder.toString();
+ }
+
+ private TeXOSQuery invoker;
+ private Pattern pattern;
+ private Vector<Path> result;
+ private Comparator<Path> comparator;
+}
diff --git a/Master/texmf-dist/source/support/texosquery/java/Manifest-jre5.txt b/Master/texmf-dist/source/support/texosquery/java/Manifest-jre5.txt
new file mode 100644
index 00000000000..f34ae37301d
--- /dev/null
+++ b/Master/texmf-dist/source/support/texosquery/java/Manifest-jre5.txt
@@ -0,0 +1,2 @@
+Main-Class: com/dickimawbooks/texosquery/TeXOSQueryJRE5
+Class-Path: .
diff --git a/Master/texmf-dist/source/support/texosquery/java/Manifest-jre7.txt b/Master/texmf-dist/source/support/texosquery/java/Manifest-jre7.txt
new file mode 100644
index 00000000000..b5669d1e008
--- /dev/null
+++ b/Master/texmf-dist/source/support/texosquery/java/Manifest-jre7.txt
@@ -0,0 +1,2 @@
+Main-Class: com/dickimawbooks/texosquery/TeXOSQueryJRE7
+Class-Path: .
diff --git a/Master/texmf-dist/source/support/texosquery/java/Manifest-jre8.txt b/Master/texmf-dist/source/support/texosquery/java/Manifest-jre8.txt
new file mode 100644
index 00000000000..0e460823ef4
--- /dev/null
+++ b/Master/texmf-dist/source/support/texosquery/java/Manifest-jre8.txt
@@ -0,0 +1,2 @@
+Main-Class: com/dickimawbooks/texosquery/TeXOSQueryJRE8
+Class-Path: .
diff --git a/Master/texmf-dist/source/support/texosquery/java/QueryAction.java b/Master/texmf-dist/source/support/texosquery/java/QueryAction.java
new file mode 100644
index 00000000000..b50e65cc3ad
--- /dev/null
+++ b/Master/texmf-dist/source/support/texosquery/java/QueryAction.java
@@ -0,0 +1,448 @@
+/*
+ Copyright (C) 2016 Nicola L.C. Talbot
+ www.dickimaw-books.com
+
+ This work may be distributed and/or modified under the
+ conditions of the LaTeX Project Public License, either version 1.3
+ of this license or (at your option) any later version.
+ The latest version of this license is in
+ http://www.latex-project.org/lppl.txt
+ and version 1.3 or later is part of all distributions of LaTeX
+ version 2005/12/01 or later.
+*/
+package com.dickimawbooks.texosquery;
+
+import java.io.Serializable;
+import java.io.ObjectOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ByteArrayInputStream;
+
+/**
+ * Class representing an action to be performed by the application.
+ * @since 1.2
+ */
+public abstract class QueryAction implements Serializable
+{
+ public QueryAction()
+ {
+ }
+
+ public QueryAction(String longForm, QueryActionType type,
+ String description)
+ {
+ this(longForm, null, 0, 0, "", type, description);
+ }
+
+ public QueryAction(String longForm, QueryActionType type,
+ String description, int minCompat)
+ {
+ this(longForm, null, 0, 0, "", type, description, minCompat);
+ }
+
+ public QueryAction(String longForm, String shortForm,
+ QueryActionType type, String description)
+ {
+ this(longForm, shortForm, 0, 0, "", type, description);
+ }
+
+ public QueryAction(String longForm, String shortForm,
+ QueryActionType type, String description, int minCompat)
+ {
+ this(longForm, shortForm, 0, 0, "", type, description, minCompat);
+ }
+
+ public QueryAction(String longForm, String shortForm, String syntax,
+ QueryActionType type, String description)
+ {
+ this(longForm, shortForm, 0, 0, syntax, type, description);
+ }
+
+ public QueryAction(String longForm, String shortForm,
+ int numOptional, int numRequired, String syntax,
+ QueryActionType type, String description)
+ {
+ this(longForm, shortForm, numOptional, numRequired,
+ syntax, type, description, 0);
+ }
+
+ public QueryAction(String longForm, String shortForm,
+ int numOptional, int numRequired, String syntax,
+ QueryActionType type, String description, int minCompat)
+ {
+ if (longForm != null)
+ {
+ longName = "--"+longForm;
+ }
+
+ if (shortForm != null)
+ {
+ shortName = "-"+shortForm;
+ }
+
+ optional = numOptional;
+ required = numRequired;
+
+ this.syntax = syntax;
+ this.type = type;
+ this.description = description;
+ this.minCompatibility = minCompat;
+ }
+
+ public boolean isAction(String name)
+ {
+ return name.equals(shortName) || name.equals(longName)
+ || (longName != null && name.startsWith(longName+"="));
+ }
+
+ public int parseArgs(String[] args, int index)
+ throws IllegalArgumentException
+ {
+ if (required > 0)
+ {
+ requiredArgs = new String[required];
+ }
+
+ if (args[index].startsWith(longName+"="))
+ {
+ int idx = args[index].indexOf("=")+1;
+ invokedName = longName;
+
+ if (idx == args[index].length())
+ {
+ if (required == 1)
+ {
+ requiredArgs[0] = "";
+ }
+ else
+ {
+ throw new IllegalArgumentException(String.format(
+ "Invalid syntax for action '%s'.%nExpected: %s",
+ invokedName, getUsage(invokedName)));
+ }
+
+ return index+1;
+ }
+
+ String[] sp;
+
+ if (required+optional > 1)
+ {
+ sp = args[index].substring(idx).split(" ");
+ }
+ else
+ {
+ sp = new String[]{args[index].substring(idx)};
+ }
+
+ idx = 0;
+
+ for (int i = 0; i < required; i++)
+ {
+ if (i >= sp.length)
+ {
+ throw new IllegalArgumentException(String.format(
+ "Invalid syntax for action '%s'.%nExpected: %s",
+ invokedName, getUsage(invokedName)));
+ }
+
+ requiredArgs[i] = sp[idx++];
+ }
+
+ optionalProvided = 0;
+
+ if (optional > 0)
+ {
+ optionalArgs = new String[optional];
+ }
+
+ for (int i = 0; i < optional; i++)
+ {
+ if (idx >= sp.length)
+ {
+ // no optional arguments, skip
+ break;
+ }
+
+ optionalArgs[optionalProvided++] = sp[idx++];
+ }
+
+ return index+1;
+ }
+ else
+ {
+ invokedName = args[index++];
+
+ for (int i = 0; i < required; i++)
+ {
+ if (index >= args.length)
+ {
+ throw new IllegalArgumentException(String.format(
+ "Invalid syntax for action '%s'.%nExpected: %s",
+ invokedName, getUsage(invokedName)));
+ }
+
+ requiredArgs[i] = args[index++];
+ }
+
+ optionalProvided = 0;
+
+ if (optional > 0)
+ {
+ optionalArgs = new String[optional];
+ }
+
+ for (int i = 0; i < optional; i++)
+ {
+ if (index >= args.length || args[index].startsWith("-"))
+ {
+ // no optional arguments, skip
+ break;
+ }
+
+ optionalArgs[optionalProvided++] = args[index++];
+ }
+ }
+
+ return index;
+ }
+
+ public int providedOptionCount()
+ {
+ return optionalProvided;
+ }
+
+ public String getOptionalArgument(int index)
+ {
+ return index < optionalArgs.length ? optionalArgs[index] : null;
+ }
+
+ public String getRequiredArgument(int index)
+ {
+ return requiredArgs[index];
+ }
+
+ public String getInvocation()
+ {
+ StringBuilder builder = new StringBuilder(getInvokedName());
+
+ if (requiredArgs != null)
+ {
+ for (String arg : requiredArgs)
+ {
+ if (arg.contains(" "))
+ {
+ builder.append(String.format(" '%s'",
+ arg.replaceAll("'", "\\\\'")));
+ }
+ else
+ {
+ builder.append(String.format(" %s", arg));
+ }
+ }
+ }
+
+ if (optionalArgs != null)
+ {
+ for (String arg : optionalArgs)
+ {
+ if (arg == null) continue;
+
+ if (arg.contains(" "))
+ {
+ builder.append(String.format(" '%s'",
+ arg.replaceAll("'", "\\\\'")));
+ }
+ else
+ {
+ builder.append(String.format(" %s", arg));
+ }
+ }
+ }
+
+ return builder.toString();
+ }
+
+ public String getInvokedName()
+ {
+ return invokedName;
+ }
+
+ public QueryActionType getType()
+ {
+ return type;
+ }
+
+ public String getUsage(String name)
+ {
+ return String.format("%s%s",
+ name, syntax == null || "".equals(syntax) ? "" : " "+syntax);
+ }
+
+ /**
+ * Formats the description to fit maximum of 80 character line
+ * width.
+ */
+ private String formatDescription()
+ {
+ int n = description.length();
+ int max = MAX_CHARS_PER_LINE-8;
+
+ if (n < max)
+ {
+ return description;
+ }
+
+ StringBuilder builder = new StringBuilder();
+ StringBuilder line = new StringBuilder();
+ StringBuilder word = new StringBuilder();
+
+ for (int i = 0, offset=1; i < n; i += offset)
+ {
+ int codepoint = description.codePointAt(i);
+ offset = Character.charCount(codepoint);
+
+ if (codepoint == ' ')
+ {
+ if (line.length()+word.length()+1 >= max)
+ {
+ builder.append(line);
+ builder.append(String.format("%n"));
+ line = new StringBuilder();
+ line.append('\t');
+ }
+
+ line.append(word);
+ line.append(' ');
+ word = new StringBuilder();
+ }
+ else
+ {
+ word.appendCodePoint(codepoint);
+ }
+ }
+
+ if (line.length()+word.length()+1 >= max)
+ {
+ builder.append(line);
+ builder.append(String.format("%n"));
+ line = new StringBuilder();
+ line.append('\t');
+ }
+
+ line.append(word);
+ builder.append(line);
+
+ return builder.toString();
+ }
+
+ public String help()
+ {
+ String usage;
+
+ if (shortName == null)
+ {
+ usage = getUsage(longName);
+ }
+ else
+ {
+ usage = String.format("%s or %s", getUsage(shortName),
+ getUsage(longName));
+ }
+
+ int n = usage.length();
+
+ return String.format("%s%n\t%s.%n", usage, formatDescription());
+ }
+
+ public String doAction(int compatible) throws IllegalArgumentException
+ {
+ if (compatible < minCompatibility)
+ {
+ throw new IllegalArgumentException(String.format(
+ "'%s' option not available in compatibility mode %d",
+ invokedName, compatible));
+ }
+
+ return action();
+ }
+
+ protected abstract String action();
+
+ /**
+ * Make a copy of this object.
+ */
+ public final QueryAction copy()
+ {
+ try
+ {
+ ByteArrayOutputStream byteStr = new ByteArrayOutputStream();
+
+ new ObjectOutputStream(byteStr).writeObject(this);
+
+ QueryAction action = (QueryAction) new ObjectInputStream(
+ new ByteArrayInputStream(byteStr.toByteArray())).readObject();
+ action.same(this);
+
+ return action;
+ }
+ catch (Exception e)
+ {
+ throw new AssertionError(e);
+ }
+ }
+
+ private void same(QueryAction action)
+ {
+ longName = action.longName;
+ shortName = action.shortName;
+ invokedName = action.invokedName;
+ syntax = action.syntax;
+ optional = action.optional;
+ optionalProvided = action.optionalProvided;
+ required = action.required;
+ type = action.type;
+ description = action.description;
+ minCompatibility = action.minCompatibility;
+
+ if (action.optionalArgs != null)
+ {
+ optionalArgs = new String[action.optionalArgs.length];
+
+ for (int i = 0; i < action.optionalArgs.length; i++)
+ {
+ optionalArgs[i] = action.optionalArgs[i];
+ }
+ }
+
+ if (action.requiredArgs != null)
+ {
+ requiredArgs = new String[action.requiredArgs.length];
+
+ for (int i = 0; i < action.requiredArgs.length; i++)
+ {
+ requiredArgs[i] = action.requiredArgs[i];
+ }
+ }
+ }
+
+ public String getLongName()
+ {
+ return longName;
+ }
+
+ private String longName=null;
+ private String shortName=null;
+ private String invokedName=null;
+ private String syntax="";
+ private int optional=0;
+ private int optionalProvided=0;
+ private int required=0;
+ private String[] requiredArgs=null;
+ private String[] optionalArgs=null;
+ private QueryActionType type;
+ private String description;
+ private int minCompatibility=0;
+
+ private static final int MAX_CHARS_PER_LINE=80;
+}
diff --git a/Master/texmf-dist/source/support/texosquery/java/QueryActionType.java b/Master/texmf-dist/source/support/texosquery/java/QueryActionType.java
new file mode 100644
index 00000000000..0eb2382766d
--- /dev/null
+++ b/Master/texmf-dist/source/support/texosquery/java/QueryActionType.java
@@ -0,0 +1,19 @@
+/*
+ Copyright (C) 2016 Nicola L.C. Talbot
+ www.dickimaw-books.com
+
+ This work may be distributed and/or modified under the
+ conditions of the LaTeX Project Public License, either version 1.3
+ of this license or (at your option) any later version.
+ The latest version of this license is in
+ http://www.latex-project.org/lppl.txt
+ and version 1.3 or later is part of all distributions of LaTeX
+ version 2005/12/01 or later.
+*/
+package com.dickimawbooks.texosquery;
+
+public enum QueryActionType
+{
+ FILE_ACTION, LOCALE_ACTION, GENERAL_ACTION
+}
+
diff --git a/Master/texmf-dist/source/support/texosquery/java/TeXOSQuery.java b/Master/texmf-dist/source/support/texosquery/java/TeXOSQuery.java
new file mode 100644
index 00000000000..882cd3afb44
--- /dev/null
+++ b/Master/texmf-dist/source/support/texosquery/java/TeXOSQuery.java
@@ -0,0 +1,4459 @@
+/*
+ Copyright (C) 2016 Nicola L.C. Talbot
+ www.dickimaw-books.com
+
+ This work may be distributed and/or modified under the
+ conditions of the LaTeX Project Public License, either version 1.3
+ of this license or (at your option) any later version.
+ The latest version of this license is in
+ http://www.latex-project.org/lppl.txt
+ and version 1.3 or later is part of all distributions of LaTeX
+ version 2005/12/01 or later.
+*/
+package com.dickimawbooks.texosquery;
+
+import java.io.BufferedReader;
+import java.util.Locale;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.TimeZone;
+import java.util.Vector;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.text.DecimalFormatSymbols;
+import java.text.Format;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.text.DateFormatSymbols;
+import java.text.NumberFormat;
+import java.text.DecimalFormat;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Serializable;
+
+/**
+ * Application functions. These methods need to be Java version 1.5
+ * compatible. The 1.7 methods need to be in the TeXOSQueryJRE7 class
+ * (which provides the main part of texosquery.jar) and the 1.8 methods in
+ * TeXOSQueryJRE8 (which provides the main part of texosquery-jre8.jar).
+ *
+ * The texosquery-jre5.jar version should not be considered secure
+ * and is only provided for antiquated systems.
+ * Java 5 and 6 are both deprecated and are now considered security
+ * risks.
+ *
+ * Since this application is designed to be run from TeX, the output
+ * needs to be easy to parse using TeX commands. For this reason,
+ * most exceptions are caught and an empty string is returned. The
+ * TeX code can then check for an empty value to determine failure.
+ * There's a debug mode to print error messages to STDERR to
+ * investigate the reason for failure.
+ * @author Nicola Talbot
+ * @version 1.2
+ * @since 1.0
+ */
+public class TeXOSQuery implements Serializable
+{
+ /**
+ * Constructor.
+ * @param name The application name.
+ */
+ public TeXOSQuery(String name)
+ {
+ this.name = name;
+ }
+
+ /**
+ * Gets the application name.
+ * @return the application name
+ * @since 1.2
+ */
+ public String getName()
+ {
+ return name;
+ }
+
+ /**
+ * Runs kpsewhich and returns the result. This is for single
+ * argument lookups through kpsewhich, such as a file location
+ * or variable value.
+ * @param arg The argument to pass to kpsewhich
+ * @return The result read from the first line of STDIN
+ * @since 1.2
+ */
+ protected String kpsewhich(String arg)
+ throws IOException,InterruptedException
+ {
+ // Create and start the process.
+ Process process =
+ new ProcessBuilder("kpsewhich", arg).start();
+
+ int exitCode = process.waitFor();
+
+ String line = null;
+
+ if (exitCode == 0)
+ {
+ // kpsewhich completed with exit code 0.
+ // Read STDIN to find the result.
+
+ InputStream stream = process.getInputStream();
+
+ if (stream == null)
+ {
+ throw new IOException(String.format(
+ "Unable to open input stream from process: kpsewhich '%s'",
+ arg));
+ }
+
+ BufferedReader reader = null;
+
+ try
+ {
+ reader = new BufferedReader(new InputStreamReader(stream));
+
+ // only read a single line, nothing further is required
+ // for a variable or file location query.
+ line = reader.readLine();
+ }
+ finally
+ {
+ if (reader != null)
+ {
+ reader.close();
+ }
+ }
+ }
+ else
+ {
+ // kpsewhich failed.
+
+ throw new IOException(String.format(
+ "\"kpsewhich '%s'\" failed with exit code: %d", arg, exitCode));
+ }
+
+ return line;
+ }
+
+ /**
+ * Print message if in debug mode. Message is printed to STDERR
+ * if the debug level is greater than or equal to the given level.
+ * Debugging messages are all written to STDERR rather than
+ * STDOUT so they show up in the transcript rather than being
+ * captured by the shell escape.
+ * @param message Debugging message.
+ * @param level Debugging level.
+ * @since 1.2
+ */
+ public void debug(String message, int level)
+ {
+ if (debugLevel >= level)
+ {
+ System.err.println(String.format("%s: %s", name, message));
+ }
+ }
+
+ /**
+ * Print message if in debug mode. Message is printed to STDERR
+ * if the debug level is 1 or more.
+ * @param message Debugging message.
+ * @since 1.2
+ */
+ public void debug(String message)
+ {
+ debug(message, DEBUG_ERROR_LEVEL);
+ }
+
+ /**
+ * Message if in debug mode. This is for information rather than
+ * errors. The message is printed to STDERR if the debug level
+ * is 3 or more.
+ * @param message Debugging message.
+ * @since 1.2
+ */
+ public void info(String message)
+ {
+ debug(message, DEBUG_INFO_LEVEL);
+ }
+
+ /**
+ * Print message and exception if in debug mode. Message is printed to
+ * STDERR if the debug level is greater than or equal to the given level.
+ * The exception may be null. If not null, the exception message
+ * is printed.
+ * @param message Debugging message.
+ * @param excpt Exception.
+ * @param msgLevel Debugging level for message.
+ * @param traceLevel Debugging level for stack trace.
+ * @since 1.2
+ */
+ public void debug(String message, Throwable excpt, int msgLevel,
+ int traceLevel)
+ {
+ debug(message, msgLevel);
+
+ if (excpt != null)
+ {
+ debug(excpt.getMessage(), msgLevel);
+
+ if (debugLevel >= traceLevel)
+ {
+ excpt.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Print message and exception if in debug mode. The message
+ * level is 1 and the trace level is 2.
+ * @param message Debugging message.
+ * @param excpt Exception.
+ * @since 1.2
+ */
+ public void debug(String message, Throwable excpt)
+ {
+ debug(message, excpt, DEBUG_ERROR_LEVEL, DEBUG_STACK_TRACE_LEVEL);
+ }
+
+ /**
+ * Checks if file is in or below the given directory. This might
+ * be easier with java.nio.file.Path etc but that requires Java
+ * 1.7, so use the old-fashioned method.
+ * @param file The file being checked
+ * @param dir The directory being searched
+ * @return true if found
+ * @since 1.2
+ */
+ protected boolean isFileInTree(File file, File dir)
+ throws IOException
+ {
+ if (file == null || dir == null) return false;
+
+ file = file.getCanonicalFile();
+ dir = dir.getCanonicalFile();
+
+ File parent = file.getParentFile();
+
+ while (parent != null)
+ {
+ if (parent.equals(dir))
+ {
+ return true;
+ }
+
+ parent = parent.getParentFile();
+ }
+
+ return false;
+ }
+
+ /**
+ * Determine if the given file is hidden.
+ * Java's File.isHidden() method seems to consider "." and ".."
+ * as hidden directories, so this method converts the file to a
+ * canonical path before testing.
+ * @param file The file to check
+ * @return True if the file is considered hidden.
+ * @since 1.2
+ */
+ public boolean isHidden(File file)
+ {
+ try
+ {
+ return file.getCanonicalFile().isHidden();
+ }
+ catch (IOException e)
+ {
+ // file can't be converted to a canonical path, so
+ // consider it hidden
+
+ debug(String.format(
+ "Unable to convert file to a canonical path: ",
+ file.toString()), e);
+ }
+
+ return true;
+ }
+
+ /**
+ * Queries if the given file may be read according to
+ * openin_any. Since the user may not require any of the file
+ * access functions, the openin variable is only set the first
+ * time this method is used to reduce unnecessary overhead.
+ * kpsewhich is used to lookup the value of openin_any, which
+ * may have one of the following values: a (any), r (restricted,
+ * no hidden files) or p (paranoid, as restricted and no parent
+ * directories and no absolute paths except under $TEXMFOUTPUT)
+ * @param file The file to be checked
+ * @return true if read-access allowed
+ * @since 1.2
+ */
+ public boolean isReadPermitted(File file)
+ {
+ // if file doesn't exist, it can't be read
+ if (file == null || !file.exists())
+ {
+ return false;
+ }
+
+ try
+ {
+ if (openin == OPENIN_UNSET)
+ {
+ //First time this method has been called. Use kpsewhich
+ //to determine the value.
+
+ try
+ {
+ String result = kpsewhich("-var-value=openin_any");
+
+ if ("a".equals(result))
+ {
+ openin=OPENIN_A;
+ }
+ else if ("r".equals(result))
+ {
+ openin=OPENIN_R;
+ }
+ else if ("p".equals(result))
+ {
+ openin=OPENIN_P;
+ }
+ else
+ {
+ // This shouldn't occur, but just in case...
+ debug(String.format("Invalid openin_any value: %s",
+ result));
+ openin = OPENIN_P;
+ }
+ }
+ catch (Exception e)
+ {
+ // kpsewhich failed, assume paranoid
+ debug("Can't determine openin value, assuming 'p'", e);
+ openin = OPENIN_P;
+ }
+
+ // Now find TEXMFOUTPUT if set (only need this with the
+ // paranoid setting)
+
+ if (openin == OPENIN_P)
+ {
+ String path = null;
+
+ try
+ {
+ path = System.getenv("TEXMFOUTPUT");
+ }
+ catch (SecurityException e)
+ {
+ debug("Can't query TEXMFOUTPUT", e);
+ }
+
+ if (path != null && !"".equals(path))
+ {
+ texmfoutput = new File(fromTeXPath(path));
+
+ if (!texmfoutput.exists())
+ {
+ debug(String.format(
+ "TEXMFOUTPUT doesn't exist, ignoring: %s",
+ texmfoutput.toString()));
+ texmfoutput = null;
+ }
+ else if (!texmfoutput.isDirectory())
+ {
+ debug(String.format(
+ "TEXMFOUTPUT isn't a directory, ignoring: %s",
+ texmfoutput.toString()));
+ texmfoutput = null;
+ }
+ else if (!texmfoutput.canRead())
+ {
+ debug(String.format(
+ "TEXMFOUTPUT doesn't have read permission, ignoring: %s",
+ texmfoutput.toString()));
+ texmfoutput = null;
+ }
+ }
+ }
+ }
+
+ // Now check if the given file can be read according to the
+ // openin setting.
+
+ switch (openin)
+ {
+ case OPENIN_A:
+ // any file can be read as long as the OS allows it
+ return file.canRead();
+ case OPENIN_P:
+ // paranoid check
+
+ if (isFileInTree(file, texmfoutput))
+ {
+ // file under TEXMFOUTPUT, so it's okay as long
+ // as it has read permission
+ return file.canRead();
+ }
+
+ // does the file have an absolute path?
+
+ if (file.isAbsolute())
+ {
+ debug(String.format(
+ "Read access forbidden by openin_any=%c (has absolute path outside TEXMFOUTPUT): %s",
+ openin, file));
+ return false;
+ }
+
+ // is the file outside the cwd?
+ File cwd = new File(getSystemProperty("user.dir", "."));
+
+ if (file.getParentFile() != null && !isFileInTree(file, cwd))
+ {
+ debug(String.format(
+ "Read access forbidden by openin_any=%c (outside cwd path): %s",
+ openin, file));
+ return false;
+ }
+
+ // no break, fall through to restricted check
+ case OPENIN_R:
+
+ if (isHidden(file))
+ {
+ // hidden file so not permitted
+ debug(String.format(
+ "Read access forbidden by openin_any=%c (hidden file): %s",
+ openin, file));
+ return false;
+ }
+
+ break;
+ default:
+ // this shouldn't happen, but just in case...
+ debug(String.format("Invalid openin value: %d", openin));
+ // don't allow, something's gone badly wrong
+ return false;
+ }
+
+ // return read access
+ return file.canRead();
+ }
+ catch (Exception e)
+ {
+ // Catch all exceptions
+ debug(String.format("Read permission check failed: %s", file), e);
+
+ // Can't permit read if something's gone wrong here.
+ return false;
+ }
+ }
+
+ /**
+ * Gets the given system property or the default value.
+ * Returns the default value if the property isn't set or can't be accessed.
+ * @param propName The property name
+ * @param defValue The default value
+ * @return The property value or the default if unavailable
+ * @since 1.2
+ */
+ public String getSystemProperty(String propName, String defValue)
+ {
+ try
+ {
+ return System.getProperty(propName, defValue);
+ }
+ catch (SecurityException e)
+ {
+ // The security manager doesn't permit access to this property.
+
+ debug(String.format("Unable to access property: %s", propName), e);
+ return defValue;
+ }
+ }
+
+ /**
+ * Escapes potentially problematic characters from a string that will be
+ * expanded when input by TeX's shell escape.
+ *
+ * Some of the methods in this class return TeX code. Those
+ * returned values shouldn't be escaped as it would interfere
+ * with the code, so just use this method on information
+ * directly obtained from Java. This will typically be either
+ * file names (in which case the characters within the string
+ * must all be "letter" or "other") or regular text for use in
+ * the document (such as dates or times, in which case the
+ * characters may be active to allow them to be correctly
+ * typeset, such as UTF-8 characters with inputenc.sty).
+ *
+ * The date-time and numeric patterns (such as "YYYY-MM-DD"
+ * or "#,##0.0") are dealt with elsewhere as they need different treatment.
+ *
+ * \\TeXOSQuery locally defines commands for characters
+ * used in file names (catcode 12). These are all in the form
+ * \\fxxx (such as \\fhsh for a literal hash). Since the
+ * texosquery.tex code is designed to be generic we can't assume
+ * the eTeX \\detokenize primitive is available. This does,
+ * however, assume that the document author hasn't changed the
+ * category codes of the ASCII alphanumerics, but that ought to
+ * be a safe assumption.
+ *
+ * We also have commands for characters intended for use in document
+ * text, which shouldn't be interpreted literally. These are all
+ * in the form \\txxx (such as \\thsh which should expand to
+ * \#).
+ *
+ * The regular space \\tspc guards against a space occurring after
+ * a character that needs to be converted to a control sequence.
+ * (For example "# 1" becomes "\\thsh \\tspc 1")
+ * There's also a literal space \\fspc to guard against spaces
+ * in file names.
+ *
+ * This should take care of any insane file-naming schemes, such
+ * as <tt>bad~file name#1.tex</tt>, <tt>stupid {file} name.tex</tt>,
+ * <tt style="white-space: pre;">spaced out file #2.tex</tt>,
+ * <tt>file's stupid name.tex</tt>.
+ *
+ * To help protect against input encoding problems, non-ASCII
+ * characters are wrapped in \\twrp (regular text) or \\fwrp
+ * (file names). \\TeXOSQuery locally redefines these to
+ * \\texosquerynonasciiwrap and \\texosquerynonasciidetokwrap
+ * which may be used to provide some protection or conversion from one
+ * encoding to another, if required.
+ *
+ * For example, the language "fran&#231;ais" would be returned as
+ * "fran\\twrp{&#231;}ais", which can be typeset directly with
+ * XeTeX or LuaTeX or through active characters with
+ * inputenc.sty, but the directory called <tt>Fran&#231;cois</tt> would be
+ * returned as <tt>Fran\\fwrp{&#231;}cois</tt>, which will try to
+ * detokenize the &#231; character.
+ *
+ * @param string Input string.
+ * @param isRegularText true if the string represents text (for example,
+ * month names), set to false if string is something literal,
+ * such as a file name.
+ * @return The processed string
+ * @since 1.2
+ */
+ public String escapeSpChars(String string, boolean isRegularText)
+ {
+ if (compatible < 2)
+ {
+ return escapeHash(string);
+ }
+
+ StringBuilder builder = new StringBuilder();
+
+ // This iterates over Unicode characters so we can't use a simple
+ // i++ increment. The offset is obtained from Character.charCount
+ for (int i = 0, n = string.length(); i < n; )
+ {
+ int codepoint = string.codePointAt(i);
+ i += Character.charCount(codepoint);
+
+ builder.append(escapeSpChars(codepoint, isRegularText));
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Escapes file name. This should already have had the directory
+ * divider changed to a forward slash where necessary.
+ * @param filename Input string.
+ * @return String with characters escaped.
+ * @since 1.2
+ */
+ public String escapeFileName(String filename)
+ {
+ return escapeSpChars(filename, false);
+ }
+
+ /**
+ * Escapes regular text.
+ * @param string Input string.
+ * @return String with characters escaped.
+ * @since 1.2
+ */
+ public String escapeText(String string)
+ {
+ return escapeSpChars(string, true);
+ }
+
+ /**
+ * Escapes regular text.
+ * @param codepoint Input Unicode character.
+ * @return String with characters escaped.
+ * @since 1.2
+ */
+ public String escapeText(int codepoint)
+ {
+ return escapeSpChars(codepoint, true);
+ }
+
+ /**
+ * Escapes the given Unicode character.
+ * All ASCII punctuation characters have a literal and textual
+ * command to represent them in file names and document text,
+ * respectively. The literal (file name) commands are prefixed
+ * with "f" and the textual commands are prefixed with "t".
+ * None of the control codes should appear in any of the
+ * results, but they are checked for completeness.
+ * @param codePoint Input code point.
+ * @param isRegularText true if the character is in a string representing
+ * text, set to false if string is a file name etc
+ * @return String with character escaped.
+ * @since 1.2
+ */
+ public String escapeSpChars(int codepoint, boolean isRegularText)
+ {
+ return escapeSpChars(codepoint, isRegularText ? "t" : "f");
+ }
+
+ /**
+ * Escapes the given Unicode character.
+ * As above but with the prefix supplied.
+ * @param codePoint Input code point.
+ * @param prefix The control sequence name prefix.
+ * @return String with character escaped.
+ * @since 1.2
+ */
+ public String escapeSpChars(int codepoint, String prefix)
+ {
+ switch (codepoint)
+ {
+ case '!': return String.format("\\%sexc ", prefix);
+ case '"': return String.format("\\%sdqt ", prefix);
+ case '#': return String.format("\\%shsh ", prefix);
+ case '$': return String.format("\\%sdol ", prefix);
+ case '%': return String.format("\\%spct ", prefix);
+ case '&': return String.format("\\%samp ", prefix);
+ case '\'': return String.format("\\%sapo ", prefix);
+ case '(': return String.format("\\%sopb ", prefix);
+ case ')': return String.format("\\%sclb ", prefix);
+ case '*': return String.format("\\%sast ", prefix);
+ case '+': return String.format("\\%spls ", prefix);
+ case ',': return String.format("\\%scom ", prefix);
+ case '-': return String.format("\\%shyn ", prefix);
+ case '.': return String.format("\\%sdot ", prefix);
+ case '/': return String.format("\\%sslh ", prefix);
+ case ':': return String.format("\\%scln ", prefix);
+ case ';': return String.format("\\%sscl ", prefix);
+ case '<': return String.format("\\%sles ", prefix);
+ case '=': return String.format("\\%seql ", prefix);
+ case '>': return String.format("\\%sgre ", prefix);
+ case '?': return String.format("\\%sque ", prefix);
+ case '@': return String.format("\\%satc ", prefix);
+ case '[': return String.format("\\%sosb ", prefix);
+ case '\\': return String.format("\\%sbks ", prefix);
+ case ']': return String.format("\\%scsb ", prefix);
+ case '^': return String.format("\\%scir ", prefix);
+ case '_': return String.format("\\%susc ", prefix);
+ case '`': return String.format("\\%sgrv ", prefix);
+ case '{': return String.format("\\%slbr ", prefix);
+ case '}': return String.format("\\%srbr ", prefix);
+ case '~': return String.format("\\%stld ", prefix);
+ case ' ': return String.format("\\%sspc ", prefix);
+ // These next few cases shouldn't occur, but
+ // check for them anyway.
+ case 0x007F: return ""; // delete control
+ case 0x0009: return "^^I";// tab
+ case 0x000A: // lf (fall through to cr)
+ case 0x000C: // ff
+ case 0x000D: return " "; // cr
+ default:
+
+ if (codepoint < 32)
+ {
+ return ""; // strip control characters
+ }
+ else if (codepoint >= 32 && codepoint <= 126)
+ {
+ // ASCII letters and digits (all ASCII punctuation
+ // dealt with above).
+ return String.format("%c", codepoint);
+ }
+ else
+ {
+ // Outside Basic Latin set.
+ return String.format("\\%swrp{%c}", prefix, codepoint);
+ }
+ }
+ }
+
+ /**
+ * Escapes any hashes in input string.
+ * Now only used if compatibility level is less than 2 (pre
+ * texosquery version 1.2).
+ * @param string Input string.
+ * @return String with hash escaped.
+ */
+ public static String escapeHash(String string)
+ {
+ return string.replaceAll("#", "\\\\#");
+ }
+
+ /**
+ * Escapes hash from input character.
+ * No longer required.
+ * @param c Input character.
+ * @return String with hash escaped.
+ */
+ public static String escapeHash(char c)
+ {
+ return String.format("%s", c == '#' ? "\\#" : c);
+ }
+
+ /**
+ * Gets the OS name. As far as I can tell, the "os.name"
+ * property should return a string that just contains Basic
+ * Latin upper or lower case letters, so we don't need to worry
+ * about special characters.
+ * @return The OS name as string.
+ */
+ public String getOSname()
+ {
+ return getSystemProperty("os.name", "");
+ }
+
+ /**
+ * Gets the OS architecture. As with the OS name, this shouldn't
+ * contain any special characters.
+ * @return The OS architecture as string.
+ */
+ public String getOSarch()
+ {
+ return getSystemProperty("os.arch", "");
+ }
+
+ /**
+ * Gets the OS version. This may contain an underscore, so treat
+ * it like a file name.
+ * @return The OS version as string.
+ */
+ public String getOSversion()
+ {
+ return escapeFileName(getSystemProperty("os.version", ""));
+ }
+
+ /**
+ * Converts the filename string to TeX path. Since this is designed to work
+ * within TeX, backslashes in paths need to be replaced with forward
+ * slashes.
+ * @param filename The filename string.
+ * @return TeX path.
+ */
+ public String toTeXPath(String filename)
+ {
+ if (filename == null)
+ {
+ // This shouldn't happen, but just in case...
+ try
+ {
+ // throw so we can get a stack trace for debugging
+ throw new NullPointerException();
+ }
+ catch (NullPointerException e)
+ {
+ debug("null file name", e);
+ }
+
+ return "";
+ }
+
+ // If the OS uses backslash as the directory divider,
+ // convert all backslashes to forward slashes. The Java regex
+ // means that we need four backslashes to represent a single literal
+ // backslash.
+
+ if (File.separatorChar == BACKSLASH)
+ {
+ filename = filename.replaceAll("\\\\", "/");
+ }
+
+ return escapeFileName(filename);
+ }
+
+ /**
+ * Converts the TeX path to the OS representation.
+ * The file name will typically be passed as a parameter through
+ * \\TeXOSQuery so it will have forward slashes as the directory
+ * divider regardless of the OS (as per \\input and
+ * \\includegraphics). This method converts the TeX file name
+ * into one that's valid for the OS.
+ * @param filename The filename string.
+ * @return The OS representation.
+ */
+ public String fromTeXPath(String filename)
+ {
+ if (filename == null)
+ {
+ // This shouldn't happen, but just in case...
+ try
+ {
+ throw new NullPointerException();
+ }
+ catch (NullPointerException e)
+ {
+ debug("null file name", e);
+ }
+
+ return "";
+ }
+
+ if (compatible < 2)
+ {
+ if (File.separatorChar == BACKSLASH)
+ {
+ return filename.replaceAll("/", "\\\\");
+ }
+
+ return filename;
+ }
+
+ // The file name may contain awkward characters. For example,
+ // the user may have a file called imagefile#1.png and
+ // they're trying to do, say,
+ // \TeXOSQuery{\result}{-p imagefile#1.png}
+ // If the shell escape is using bash, the hash will be
+ // interpreted as a comment character, so the argument
+ // received by texosquery will actually be "imagefile"
+ // since the "#1.png" part will be interpreted as a comment.
+
+ // The user can protect the # from the shell using
+ // \TeXOSQuery{\result}{-p imagefile\string\#1.png}
+ // which bash will pass as 'imagefile#1.png', but
+ // perhaps another type of shell might pass it literally
+ // as 'imagefile\#1.png', so the following allows for
+ // that by simply stripping all backslashes from the file name.
+ // (The file name is always supplied with forward slashes as
+ // the directory divider regardless of the operating system.)
+ // We can substitute the divider at this point as well.
+
+ StringBuilder builder = new StringBuilder();
+
+ for (int i = 0, n = filename.length(), offset=1; i < n; i+=offset)
+ {
+ int codepoint = filename.codePointAt(i);
+ offset = Character.charCount(codepoint);
+
+ int nextIndex = i+offset;
+ int nextCodePoint = (nextIndex<n ? filename.codePointAt(nextIndex):0);
+
+ if (codepoint == '/')
+ {
+ builder.appendCodePoint(File.separatorChar);
+ }
+ if (codepoint == BACKSLASH)
+ {
+ // Would anyone really want a literal backslash in a
+ // file name? Allow a double backslash to represent a
+ // literal backslash but only if the OS directory
+ // divider isn't a backslash. Otherwise discard
+ // this character.
+
+ if (File.separatorChar != BACKSLASH && nextCodePoint == BACKSLASH)
+ {
+ builder.appendCodePoint(codepoint);
+ i = nextIndex;
+ offset = Character.charCount(nextCodePoint);
+ }
+ else if (nextCodePoint == '/')
+ {
+ // Would anyone want a literal forward slash? Allow a
+ // slash to be escaped just in case.
+ builder.appendCodePoint('/');
+ i = nextIndex;
+ offset = Character.charCount(nextCodePoint);
+ }
+ }
+ else
+ {
+ builder.appendCodePoint(codepoint);
+ }
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Gets a file representation from a filename string. If the
+ * provided file doesn't have a parent and if it's not found in the
+ * current directory, kpsewhich will be used to locate it on
+ * TeX's path. The provided file name is assumed to have been
+ * passed through commands provided by texosquery.tex so the
+ * directory dividers should be forward slashes, even if the OS
+ * uses backslashes. The returned file may not exist. Any method
+ * that uses this method needs to check for existence.
+ * @param filename Filename string.
+ * @return File representation
+ * @since 1.2
+ */
+ public File fileFromTeXPath(String filename)
+ {
+ // Convert from TeX to the OS path representation.
+ filename = fromTeXPath(filename);
+
+ File file = new File(filename);
+
+ if (!file.exists() && file.getParent() == null)
+ {
+ // If the file doesn't exist and it doesn't have a parent
+ // directory, use kpsewhich to find it.
+
+ try
+ {
+ String result = kpsewhich(filename);
+
+ if (result != null && !"".equals(result))
+ {
+ file = new File(fromTeXPath(result));
+ }
+ }
+ catch (Exception exception)
+ {
+ // Catch all exceptions
+ debug(String.format("kpsewhich couldn't find the file: %s",
+ filename),
+ exception);
+
+ // The File object will be returned even though the file
+ // can't be found.
+ }
+ }
+
+ return file;
+ }
+
+ /**
+ * Gets the user's home directory.
+ * @return The user home as string.
+ */
+ public String getUserHome()
+ {
+ File dir = new File(getSystemProperty("user.home", ""));
+
+ if (!isReadPermitted(dir))
+ {
+ debug("Read access not permitted for the home directory");
+ return "";
+ }
+
+ // The resulting path needs to be converted to a TeX path.
+ return toTeXPath(dir.getAbsolutePath());
+ }
+
+ /**
+ * Gets the current working directory.
+ * @return The current working directory.
+ */
+ public String getCwd()
+ {
+ File dir = new File(getSystemProperty("user.dir", "."));
+
+ if (!isReadPermitted(dir))
+ {
+ // perhaps the current directory is hidden?
+ debug("Read access not permitted for the current directory");
+ return "";
+ }
+
+ // The resulting path needs to be converted to a TeX path.
+ return toTeXPath(dir.getAbsolutePath());
+ }
+
+ /**
+ * Gets the temporary directory.
+ * @return Temporary directory.
+ */
+ public String getTmpDir()
+ {
+ String filename = getSystemProperty("java.io.tmpdir", "");
+
+ if ("".equals(filename))
+ {
+ // Not set
+ return "";
+ }
+
+ File dir = new File(filename);
+
+ if (!isReadPermitted(dir))
+ {
+ debug(String.format("Read access not permitted for directory: %s",
+ dir));
+ return "";
+ }
+
+ // The resulting path needs to be converted to a TeX path.
+ return toTeXPath(filename);
+ }
+
+ /**
+ * Gets the week year for the given calendar.
+ * Calendar.getWeekYear() was added to Java 7, so this defaults
+ * to the year instead. This method needs to be overridden in
+ * TeXOSQueryJRE7 and TeXOSQueryJRE8.
+ * @return The week year
+ * @since 1.2
+ */
+ public int getWeekYear(Calendar cal)
+ {
+ return cal.get(Calendar.YEAR);
+ }
+
+ /**
+ * Converts the day of week index returned by
+ * Calendar.DAY_OF_WEEK to Monday=1 based indexing.
+ * @param index the day of week index obtained from Calendar.DAY_OF_WEEK
+ * @return index with Monday=1 as the base
+ * @since 1.2
+ */
+ private int getDayOfWeekIndex(int index)
+ {
+ switch (index)
+ {
+ case Calendar.MONDAY: return 1;
+ case Calendar.TUESDAY: return 2;
+ case Calendar.WEDNESDAY: return 3;
+ case Calendar.THURSDAY: return 4;
+ case Calendar.FRIDAY: return 5;
+ case Calendar.SATURDAY: return 6;
+ case Calendar.SUNDAY: return 7;
+ }
+
+ try
+ {
+ // this shouldn't happen
+ throw new IllegalArgumentException(
+ String.format("Invalid day of week index: %d", index));
+ }
+ catch (Exception e)
+ {
+ debug(e.getMessage(), e);
+ }
+
+ return 0;
+ }
+
+ /**
+ * Gets all the date-time data for the current date-time.
+ * @return data in format that can be read by \\texosqueryfmtdatetime
+ * @since 1.2
+ */
+ public String getDateTimeData()
+ {
+ Calendar cal = Calendar.getInstance();
+ cal.setTimeInMillis(now.getTime());
+
+ int hourH = cal.get(Calendar.HOUR_OF_DAY);
+
+ int hourk = (hourH == 0 ? 24 : hourH);
+
+ int hourK = cal.get(Calendar.HOUR);
+
+ int hourh = (hourK == 0 ? 12 : hourK);
+
+ TimeZone timeZone = cal.getTimeZone();
+ boolean isDaylightSaving = timeZone.inDaylightTime(now);
+
+ int timezoneoffset = cal.get(Calendar.ZONE_OFFSET);
+
+ if (isDaylightSaving)
+ {
+ timezoneoffset += cal.get(Calendar.DST_OFFSET);
+ }
+
+ // convert from offset millisec to hours and minutes
+ // (ignore left-over seconds and milliseconds)
+
+ int tzm = timezoneoffset/60000;
+
+ int tzh = tzm/60;
+
+ tzm = tzm % 60;
+
+ return String.format(
+ "{%d}{%d}{%d}{%d}{%d}{%d}{%d}{%d}{%d}{%d}{%d}{%d}{%d}{%d}{%d}{%d}{%d}{%d}{{%d}{%d}{%s}{%d}}",
+ cal.get(Calendar.ERA),
+ cal.get(Calendar.YEAR),
+ getWeekYear(cal),
+ cal.get(Calendar.MONTH)+1,
+ cal.get(Calendar.WEEK_OF_YEAR),
+ cal.get(Calendar.WEEK_OF_MONTH),
+ cal.get(Calendar.DAY_OF_YEAR),
+ cal.get(Calendar.DAY_OF_MONTH),
+ cal.get(Calendar.DAY_OF_WEEK_IN_MONTH),
+ getDayOfWeekIndex(cal.get(Calendar.DAY_OF_WEEK)),// Monday=1, etc
+ cal.get(Calendar.AM_PM),
+ hourH, hourk, hourK, hourh,
+ cal.get(Calendar.MINUTE),
+ cal.get(Calendar.SECOND),
+ cal.get(Calendar.MILLISECOND),
+ tzh, tzm, timeZone.getID(),
+ isDaylightSaving ? 1 : 0);
+ }
+
+ /**
+ * Get the time zone names for the given locale.
+ * The data for each zone is provided in the form
+ * {id}{short name}{long name}{short dst name}\marg{long dst name}
+ * @param localeTag The locale
+ * @return list of zone information for the locale
+ * @since 1.2
+ */
+
+ public String getTimeZones(String localeTag)
+ {
+ Locale locale;
+
+ if (localeTag == null || "".equals(localeTag))
+ {
+ locale = Locale.getDefault();
+ }
+ else
+ {
+ locale = getLocale(localeTag);
+ }
+
+ StringBuilder builder = new StringBuilder();
+
+ String[] zones = TimeZone.getAvailableIDs();
+
+ for (int i = 0; i < zones.length; i++)
+ {
+ TimeZone tz = TimeZone.getTimeZone(zones[i]);
+
+ builder.append(String.format("{{%s}{%s}{%s}{%s}{%s}}",
+ escapeFileName(tz.getID()),
+ escapeText(tz.getDisplayName(false, TimeZone.SHORT, locale)),
+ escapeText(tz.getDisplayName(false, TimeZone.LONG, locale)),
+ escapeText(tz.getDisplayName(true, TimeZone.SHORT, locale)),
+ escapeText(tz.getDisplayName(true, TimeZone.LONG, locale))));
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Gets the current date in PDF format. (The same format as
+ * \pdfcreationdate.)
+ * @return The current date.
+ */
+ public String pdfnow()
+ {
+ Calendar cal = Calendar.getInstance();
+ cal.setTimeInMillis(now.getTime());
+
+ return pdfDate(cal);
+ }
+
+ /**
+ * Gets the date in PDF format.
+ * @param calendar A calendar object.
+ * @return Date in PDF format.
+ */
+ public String pdfDate(Calendar calendar)
+ {
+ String tz = String.format("%1$tz", calendar);
+
+ if (compatible < 2)
+ {
+ return String.format(
+ "D:%1$tY%1$tm%1td%1$tH%1$tM%1$tS%2$s'%3$s'",
+ calendar,
+ tz.substring(0, 3),
+ tz.substring(3));
+ }
+ else
+ {
+ // Need to ensure D : + or - and ' have category code 12
+ // The simplest way to deal with this is to pass
+ // everything after the "D" to escapeFileName since
+ // the sign is hidden in the format.
+
+ return String.format("\\pdfd %s",
+ escapeFileName(
+ String.format(
+ ":%1$tY%1$tm%1td%1$tH%1$tM%1$tS%2$s'%3$s'",
+ calendar,
+ tz.substring(0, 3),
+ tz.substring(3))
+ ));
+
+ }
+ }
+
+ /**
+ * Gets the date of a file in PDF format.
+ * @param file File.
+ * @return The date in PDF format.
+ */
+ public String pdfDate(File file)
+ {
+ try
+ {
+ if (!file.exists())
+ {
+ debug(String.format(
+ "Unable to get timestamp for file (no such file): %s",
+ file.toString()));
+ return "";
+ }
+
+ if (!isReadPermitted(file))
+ {
+ debug(String.format("No read access for file: %s", file));
+ return "";
+ }
+
+ long millisecs = file.lastModified();
+
+ if (millisecs > ZERO)
+ {
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(millisecs);
+
+ return pdfDate(calendar);
+ }
+
+ // I/O error has occurred (already checked for file
+ // existence and read permission, so it's something weird).
+ // Perhaps the file is corrupt or the user has an eccentric OS that
+ // doesn't support file modification timestamps.
+ debug(String.format(
+ "Unable to get timestamp for file (I/O error): %s",
+ file.toString()));
+ }
+ catch (Exception exception)
+ {
+ // Catch all possible exceptions, including security
+ // exception.
+
+ debug(String.format(
+ "Unable to get timestamp for file: %s",
+ file.toString()),
+ exception);
+ }
+
+ // Unsuccessful
+ return "";
+ }
+
+ /**
+ * Gets the file length in bytes.
+ * @param file The file.
+ * @return The length as a string.
+ */
+ public String getFileLength(File file)
+ {
+ try
+ {
+ if (!file.exists())
+ {
+ debug(String.format(
+ "Unable to get the size of file (no such file): %s",
+ file.toString()));
+ return "";
+ }
+
+ if (!isReadPermitted(file))
+ {
+ debug(String.format("No read access for file: %s", file));
+ return "";
+ }
+
+ return String.format("%d", file.length());
+
+ }
+ catch (Exception exception)
+ {
+ // Catch all possible exceptions, including security
+ // exceptions.
+
+ debug(String.format("Unable to get the size of file: %s",
+ file.toString()),
+ exception);
+ }
+
+ // Unsuccessful
+ return "";
+ }
+
+ /**
+ * Sort the given list of file names. Java 8 has a better sort
+ * method so this is overridden in the TeXOSQueryJRE8 class.
+ * @param list The list of file names to be sorted
+ * @param directory The directory in which the files are
+ * contained
+ * @param sortType How to order the list
+ */
+ public void sortFileList(String[] list, File directory,
+ FileSortType sortType)
+ {
+ Arrays.sort(list, new FileSortComparator(directory, sortType));
+ }
+
+ /**
+ * Checks the directory used for file listings. The JRE5 version
+ * just returns the argument. The other versions convert the
+ * directory to a canonical path and check it's permitted. (The
+ * JRE7 and 8 versions are more restrictive.)
+ */
+ protected File checkDirectoryListing(File dir) throws IOException
+ {
+ return dir;
+ }
+
+ /**
+ * Gets the list of files from a directory. This uses
+ * getFilterFileList to filter out files prohibited by the
+ * openin_any setting. Note that the separator isn't escaped as
+ * the user may want some actual TeX code. For example, the
+ * separator might need to be a double backslash.
+ * The user will need to take the appropriate precautions
+ * to protect it from expansion during the shell escape.
+ * @param separator Separator.
+ * @param directory Directory (root not permitted).
+ * @param sortType How to sort the file list
+ * @param listType The type of files to include in the list
+ * @return List as a string.
+ */
+ public String getFileList(String separator, File directory,
+ FileSortType sortType, FileListType listType)
+ {
+ return getFilterFileList(separator, ".*", directory, sortType, listType);
+ }
+
+ /**
+ * Gets a filtered list of files from directory.
+ * Files with read access prohibited by openin_any or the OS are
+ * omitted from the list. The regular expression is anchored,
+ * so ".*foo" will only match file names that end with "foo".
+ *
+ * For security reasons, as from v1.2, the directory must have a
+ * parent (otherwise malicious code could try to perform a
+ * recursive search across the filing system, which would hog
+ * resources). To allow for backward compatibility, the insecure
+ * JRE5 version doesn't have this new restriction.
+ *
+ * @param separator Separator.
+ * @param regex Regular expression.
+ * @param directory Directory.
+ * @param sortType How to sort the file list
+ * @param listType The type of files to include in the list
+ * @return Filtered list as string.
+ */
+ public String getFilterFileList(String separator,
+ final String regex, File directory,
+ FileSortType sortType, final FileListType listType)
+ {
+ if (directory == null)
+ {
+ // shouldn't happen, but just in case...
+
+ debug("Unable to list contents (null directory)");
+ return "";
+ }
+
+ // Check for existence and that the given File is actually a directory.
+
+ if (!directory.exists())
+ {
+ debug(String.format(
+ "Unable to list contents (no such directory): %s",
+ directory.toString()));
+ return "";
+ }
+
+ if (!directory.isDirectory())
+ {
+ debug(String.format(
+ "Unable to list contents (not a directory): %s",
+ directory.toString()));
+ return "";
+ }
+
+ try
+ {
+ // security check (converts to full canonical path with JRE7 or 8)
+
+ directory = checkDirectoryListing(directory);
+ }
+ catch (Exception e)
+ {
+ debug(String.format("Unable to list contents of: %s",
+ directory.getAbsolutePath()), e);
+ return "";
+ }
+
+ if (!isReadPermitted(directory))
+ {
+ debug(String.format("No read access for directory: %s", directory));
+ return "";
+ }
+
+ if ((regex == null) || ("".equals(regex)))
+ {
+ // null or empty regular expression forbidden (use ".*" for
+ // all files, "" means only match a file with an empty
+ // filename, which doesn't make much sense).
+
+ debug("Null or empty regular expression in getFilterFileList");
+ return "";
+ }
+
+ StringBuilder builder = new StringBuilder();
+
+ try
+ {
+ String[] list = directory.list(
+ new FilenameFilter()
+ {
+ @Override
+ public boolean accept(File dir, String name)
+ {
+ File file = new File(dir, name);
+
+ if (!isReadPermitted(file))
+ {
+ debug(String.format("No read access for file: %s", file));
+ return false;
+ }
+
+ switch (listType)
+ {
+ case FILE_LIST_DIRECTORIES_ONLY:
+
+ if (!file.isDirectory()) return false;
+
+ break;
+ case FILE_LIST_REGULAR_FILES_ONLY:
+
+ if (file.isDirectory()) return false;
+
+ break;
+ }
+
+ return name.matches(regex);
+ }
+ });
+
+ if (list != null)
+ {
+ if (sortType != FileSortType.FILE_SORT_DEFAULT)
+ {
+ sortFileList(list, directory, sortType);
+ }
+
+ for (int i = 0; i < list.length; i++)
+ {
+ if (i > 0)
+ {
+ builder.append(separator);
+ }
+
+ if (list[i].contains(separator))
+ {
+ builder.append(String.format("{%s}", escapeFileName(list[i])));
+ }
+ else
+ {
+ builder.append(escapeFileName(list[i]));
+ }
+ }
+
+ }
+
+ return builder.toString();
+ }
+ catch (Exception exception)
+ {
+ // Catch all possible exceptions
+ debug(String.format("Unable to list contents of '%s' using regex: %s",
+ directory.toString(), regex),
+ exception);
+ }
+
+ // Unsuccessful
+ return "";
+ }
+
+ /**
+ * Recursive file listing. This method must have the CWD or a
+ * descendent as the starting directory. It will return list of
+ * files relative to the starting directory where the basename
+ * matches the supplied regular expression. Hidden files/directories
+ * and symbolic links are skipped regardless of the openin_any setting.
+ * Files without read access are also omitted from the list.
+ *
+ * This method requires the java.nio.file library, which was
+ * introduced in Java 7, so this isn't available for the JRE5
+ * version.
+ *
+ * @param separator separator to use in returned list
+ * @param regex regular expression used to match file basenames
+ * @param directory starting directory (must be cwd or a
+ * descendent of cwd)
+ * @return list of relative paths
+ */
+ public String walk(String separator,
+ String regex, File directory,
+ FileSortType sortType)
+ {
+ debug("walk requires at least JRE 7 version");
+ return "";
+ }
+
+ /**
+ * Gets the file URI.
+ * @param file The file.
+ * @return The URI.
+ */
+ public String fileURI(File file)
+ {
+ if (file == null)
+ {
+ // This shouldn't happen, but just in case...
+ debug("null file passed to fileURI");
+ return "";
+ }
+
+ if (!file.exists())
+ {
+ debug(String.format("can't obtain URI of file (no such file): %s",
+ file.toString()));
+ return "";
+ }
+
+ if (!isReadPermitted(file))
+ {
+ debug(String.format("No read access for file: %s", file));
+ return "";
+ }
+
+ try
+ {
+ return escapeFileName(file.getCanonicalFile().toURI().toString());
+ }
+ catch (Exception exception)
+ {
+ debug(String.format("Can't obtain URI of file: %s", file.toString()),
+ exception);
+ }
+
+ // Unsuccessful
+ return "";
+ }
+
+ /**
+ * Gets the full TeX file path name from File object.
+ * @param file The file.
+ * @return The path.
+ */
+ public String filePath(File file)
+ {
+ if (file == null)
+ {
+ // This shouldn't happen, but just in case...
+ debug("null file passed to filePath");
+ return "";
+ }
+
+ if (!file.exists())
+ {
+ debug(String.format(
+ "Can't obtain full file path (no such file): %s",
+ file.toString()));
+ return "";
+ }
+
+ if (!isReadPermitted(file))
+ {
+ debug(String.format(
+ "Can't obtain full file path (no read access): %s",
+ file.toString()));
+ return "";
+ }
+
+ try
+ {
+ return toTeXPath(file.getCanonicalPath());
+ }
+ catch (Exception exception)
+ {
+ debug(String.format(
+ "Can't obtain full path for file: %s", file.toString()),
+ exception);
+ }
+
+ // Unsuccessful
+ return "";
+ }
+
+ /**
+ * Gets the path for the file's parent.
+ * @param file The file.
+ * @return The path.
+ * @since 1.1
+ */
+ public String parentPath(File file)
+ {
+ if (file == null)
+ {
+ // This shouldn't happen, but just in case...
+ debug("null file passed to filePath");
+ return "";
+ }
+
+ if (!file.exists())
+ {
+ debug(String.format(
+ "Can't obtain full parent path for file (no such file): %s",
+ file.toString()));
+ return "";
+ }
+
+ if (!isReadPermitted(file))
+ {
+ debug(String.format(
+ "Can't obtain full path for file (no read access): %s",
+ file.toString()));
+ return "";
+ }
+
+ try
+ {
+ File parent = file.getCanonicalFile().getParentFile();
+
+ if (parent == null)
+ {
+ // No parent? If getCanonicalFile fails it throws an
+ // exception, so no parent would presumably mean the
+ // file's in the root directory.
+
+ debug(String.format(
+ "No parent found for file: %s", file.toString()));
+ return "";
+ }
+
+ return toTeXPath(parent.getAbsolutePath());
+
+ }
+ catch (Exception exception)
+ {
+ debug(String.format(
+ "Can't obtain full parent path for file: %s", file.toString()),
+ exception);
+ }
+
+ // Unsuccessful
+ return "";
+ }
+
+ /**
+ * Gets the script for the given locale. Java only introduced
+ * support for language scripts in version 1.7, so this returns
+ * null here. The Java 7 and 8 support needs to override this method.
+ * @param locale The locale
+ * @return The language script associated with the given locale or
+ * null if not available
+ * @since 1.2
+ */
+ public String getScript(Locale locale)
+ {
+ return null;
+ }
+
+ /**
+ * Gets the language tag for the given locale.
+ * @param locale The locale or null for the default locale
+ * @return The language tag
+ * @since 1.2
+ */
+ public String getLanguageTag(Locale locale)
+ {
+ if (locale == null)
+ {
+ locale = Locale.getDefault();
+ }
+
+ String tag = locale.getLanguage();
+
+ String country = locale.getCountry();
+
+ if (country != null && !"".equals(country))
+ {
+ tag = String.format("%s-%s", tag, country);
+ }
+
+ String variant = locale.getVariant();
+
+ if (variant != null && !"".equals(variant))
+ {
+ tag = String.format("%s-%s", tag, variant);
+ }
+
+ return tag;
+ }
+
+ /**
+ * Gets a string representation of the provided locale.
+ * @param locale The provided locale.
+ * @return String representation.
+ */
+ public String getLocale(Locale locale)
+ {
+ return getLocale(locale, false);
+ }
+
+ /**
+ * Gets a POSIX representation of the provided locale, converting the code
+ * set if possible. If the boolean argument is true, this
+ * attempts to convert the code set to a identifier that stands
+ * a better chance of being recognised by inputenc.sty. For
+ * example, UTF-8 will be converted to utf8. None of TeX's
+ * special characters should occur in any of the locale
+ * information, but we'd better treat it like a file name just in
+ * case.
+ * @param locale The provided locale.
+ * @param convertCodeset Boolean value to convert the code set.
+ * @return String representation.
+ */
+ public String getLocale(Locale locale, boolean convertCodeset)
+ {
+ String identifier = "";
+
+ if (locale == null)
+ {
+ // No locale provided, return empty string
+ debug("null locale");
+ return "";
+ }
+
+ String language = locale.getLanguage();
+
+ if (language == null)
+ {
+ // No language provided for the locale. The language
+ // part will be omitted from the returned string.
+ debug(String.format("No language for locale: %s", locale.toString()));
+ }
+ else
+ {
+ identifier = language;
+ }
+
+ String country = locale.getCountry();
+
+ if (country == null || "".equals(country))
+ {
+ // No country is associated with the locale. The
+ // country part will be omitted from the returned
+ // string. This is just information, not an error.
+
+ info(String.format("No region for locale: %s", locale.toString()));
+ }
+ else
+ {
+ if ("".equals(identifier))
+ {
+ // The identifier hasn't been set (no language
+ // provided), so just set it to the country code.
+ identifier = country;
+ }
+ else
+ {
+ // Append the country code to the identifier.
+ identifier = identifier.concat("-").concat(country);
+ }
+ }
+
+ String codeset = getCodeSet(convertCodeset);
+
+ identifier = identifier.concat(".").concat(codeset);
+
+ // Find the script if available. This is used as the modifier part
+ // but it's better to use a language tag if the script is
+ // needed.
+
+ String script = getScript(locale);
+
+ if (script == null || "".equals(script))
+ {
+ // Script information is missing. Ignore it.
+ // This is just an advisory message.
+
+ info(String.format("No script available for locale: %s",
+ locale.toString()));
+ }
+ else
+ {
+ // Append the script. This will be a four letter string
+ // (if it's not empty).
+ identifier = identifier.concat("@").concat(script);
+ }
+
+ return escapeFileName(identifier);
+ }
+
+ /**
+ * Gets default file encoding. (Don't escape it here or it will cause
+ * a problem when called in getLocale.)
+ * @param convertCodeset If true convert codeset to fit
+ * inputenc.sty
+ * @return the file encoding.
+ * @since 1.2
+ */
+ public String getCodeSet(boolean convertCodeset)
+ {
+ // Get the OS default file encoding or "UTF-8" if not set.
+
+ String codeset = getSystemProperty("file.encoding", "UTF-8");
+
+ // The codeset should not be null here as a default has
+ // been provided if the property is missing.
+
+ if (convertCodeset)
+ {
+ // If conversion is required, change to lower case
+ // and remove any hyphens.
+ codeset = codeset.toLowerCase().replaceAll("-", "");
+ }
+
+ return codeset;
+ }
+
+ /**
+ * Gets the two-letter alpha region code from the numeric code.
+ * (Java doesn't seem to recognise the numeric codes.)
+ * @param ISO 3166-1 numeric code
+ * @return ISO 3166-1 alpha code
+ * @since 1.2
+ */
+ public String getRegionAlpha2Code(int code)
+ {
+ switch (code)
+ {
+ case 4: return "AF";
+ case 8: return "AL";
+ case 10: return "AQ";
+ case 12: return "DZ";
+ case 16: return "AS";
+ case 20: return "AD";
+ case 24: return "AO";
+ case 28: return "AG";
+ case 31: return "AZ";
+ case 32: return "AR";
+ case 36: return "AU";
+ case 40: return "AT";
+ case 44: return "BS";
+ case 48: return "BH";
+ case 50: return "BD";
+ case 51: return "AM";
+ case 52: return "BB";
+ case 56: return "BE";
+ case 60: return "BM";
+ case 64: return "BT";
+ case 68: return "BO";
+ case 70: return "BA";
+ case 72: return "BW";
+ case 74: return "BV";
+ case 76: return "BR";
+ case 84: return "BZ";
+ case 86: return "IO";
+ case 90: return "SB";
+ case 92: return "VG";
+ case 96: return "BN";
+ case 100: return "BG";
+ case 104: return "MM";
+ case 108: return "BI";
+ case 112: return "BY";
+ case 116: return "KH";
+ case 120: return "CM";
+ case 124: return "CA";
+ case 132: return "CV";
+ case 136: return "KY";
+ case 140: return "CF";
+ case 144: return "LK";
+ case 148: return "TD";
+ case 152: return "CL";
+ case 156: return "CN";
+ case 158: return "TW";
+ case 162: return "CX";
+ case 166: return "CC";
+ case 170: return "CO";
+ case 174: return "KM";
+ case 175: return "YT";
+ case 178: return "CG";
+ case 180: return "CD";
+ case 184: return "CK";
+ case 188: return "CR";
+ case 191: return "HR";
+ case 192: return "CU";
+ case 196: return "CY";
+ case 203: return "CZ";
+ case 204: return "BJ";
+ case 208: return "DK";
+ case 212: return "DM";
+ case 214: return "DO";
+ case 218: return "EC";
+ case 222: return "SV";
+ case 226: return "GQ";
+ case 231: return "ET";
+ case 232: return "ER";
+ case 233: return "EE";
+ case 234: return "FO";
+ case 238: return "FK";
+ case 239: return "GS";
+ case 242: return "FJ";
+ case 246: return "FI";
+ case 248: return "AX";
+ case 250: return "FR";
+ case 254: return "GF";
+ case 258: return "PF";
+ case 260: return "TF";
+ case 262: return "DJ";
+ case 266: return "GA";
+ case 268: return "GE";
+ case 270: return "GM";
+ case 275: return "PS";
+ case 276: return "DE";
+ case 288: return "GH";
+ case 292: return "GI";
+ case 296: return "KI";
+ case 300: return "GR";
+ case 304: return "GL";
+ case 308: return "GD";
+ case 312: return "GP";
+ case 316: return "GU";
+ case 320: return "GT";
+ case 324: return "GN";
+ case 328: return "GY";
+ case 332: return "HT";
+ case 334: return "HM";
+ case 336: return "VA";
+ case 340: return "HN";
+ case 344: return "HK";
+ case 348: return "HU";
+ case 352: return "IS";
+ case 356: return "IN";
+ case 360: return "ID";
+ case 364: return "IR";
+ case 368: return "IQ";
+ case 372: return "IE";
+ case 376: return "IL";
+ case 380: return "IT";
+ case 384: return "CI";
+ case 388: return "JM";
+ case 392: return "JP";
+ case 398: return "KZ";
+ case 400: return "JO";
+ case 404: return "KE";
+ case 408: return "KP";
+ case 410: return "KR";
+ case 414: return "KW";
+ case 417: return "KG";
+ case 418: return "LA";
+ case 422: return "LB";
+ case 426: return "LS";
+ case 428: return "LV";
+ case 430: return "LR";
+ case 434: return "LY";
+ case 438: return "LI";
+ case 440: return "LT";
+ case 442: return "LU";
+ case 446: return "MO";
+ case 450: return "MG";
+ case 454: return "MW";
+ case 458: return "MY";
+ case 462: return "MV";
+ case 466: return "ML";
+ case 470: return "MT";
+ case 474: return "MQ";
+ case 478: return "MR";
+ case 480: return "MU";
+ case 484: return "MX";
+ case 492: return "MC";
+ case 496: return "MN";
+ case 498: return "MD";
+ case 499: return "ME";
+ case 500: return "MS";
+ case 504: return "MA";
+ case 508: return "MZ";
+ case 512: return "OM";
+ case 516: return "NA";
+ case 520: return "NR";
+ case 524: return "NP";
+ case 528: return "NL";
+ case 531: return "CW";
+ case 533: return "AW";
+ case 534: return "SX";
+ case 535: return "BQ";
+ case 540: return "NC";
+ case 548: return "VU";
+ case 554: return "NZ";
+ case 558: return "NI";
+ case 562: return "NE";
+ case 566: return "NG";
+ case 570: return "NU";
+ case 574: return "NF";
+ case 578: return "NO";
+ case 580: return "MP";
+ case 581: return "UM";
+ case 583: return "FM";
+ case 584: return "MH";
+ case 585: return "PW";
+ case 586: return "PK";
+ case 591: return "PA";
+ case 598: return "PG";
+ case 600: return "PY";
+ case 604: return "PE";
+ case 608: return "PH";
+ case 612: return "PN";
+ case 616: return "PL";
+ case 620: return "PT";
+ case 624: return "GW";
+ case 626: return "TL";
+ case 630: return "PR";
+ case 634: return "QA";
+ case 638: return "RE";
+ case 642: return "RO";
+ case 643: return "RU";
+ case 646: return "RW";
+ case 652: return "BL";
+ case 654: return "SH";
+ case 659: return "KN";
+ case 660: return "AI";
+ case 662: return "LC";
+ case 663: return "MF";
+ case 666: return "PM";
+ case 670: return "VC";
+ case 674: return "SM";
+ case 678: return "ST";
+ case 682: return "SA";
+ case 686: return "SN";
+ case 688: return "RS";
+ case 690: return "SC";
+ case 694: return "SL";
+ case 702: return "SG";
+ case 703: return "SK";
+ case 704: return "VN";
+ case 705: return "SI";
+ case 706: return "SO";
+ case 710: return "ZA";
+ case 716: return "ZW";
+ case 724: return "ES";
+ case 728: return "SS";
+ case 729: return "SD";
+ case 732: return "EH";
+ case 740: return "SR";
+ case 744: return "SJ";
+ case 748: return "SZ";
+ case 752: return "SE";
+ case 756: return "CH";
+ case 760: return "SY";
+ case 762: return "TJ";
+ case 764: return "TH";
+ case 768: return "TG";
+ case 772: return "TK";
+ case 776: return "TO";
+ case 780: return "TT";
+ case 784: return "AE";
+ case 788: return "TN";
+ case 792: return "TR";
+ case 795: return "TM";
+ case 796: return "TC";
+ case 798: return "TV";
+ case 800: return "UG";
+ case 804: return "UA";
+ case 807: return "MK";
+ case 818: return "EG";
+ case 826: return "GB";
+ case 831: return "GG";
+ case 832: return "JE";
+ case 833: return "IM";
+ case 834: return "TZ";
+ case 840: return "US";
+ case 850: return "VI";
+ case 854: return "BF";
+ case 858: return "UY";
+ case 860: return "UZ";
+ case 862: return "VE";
+ case 876: return "WF";
+ case 882: return "WS";
+ case 887: return "YE";
+ case 894: return "ZM";
+ }
+
+ // not recognised, return the code as a string
+ debug(String.format("Unrecognised numeric region code: %d", code));
+ return String.format("%d", code);
+ }
+
+ /**
+ * Gets the locale from the given language tag. Since Java didn't
+ * support BCP47 language tags until v1.7, we can't use
+ * Locale.forLanguageTag(String) here. (The Java 7 and 8 support
+ * will need to override this method.) Only parse for language
+ * code, country code and variant. Grandfathered, irregular and private
+ * tags not supported.
+ * @param languageTag The language tag
+ * @return The locale that closest matches the language tag
+ * @since 1.2
+ */
+ public Locale getLocale(String languageTag)
+ {
+ // The BCP47 syntax is described in
+ // https://tools.ietf.org/html/bcp47#section-2.1
+ // This is a match for a subset of the regular syntax.
+ // Only the language tag, the region and the variant are
+ // captured.
+ // Note: named capturing groups was introduced in Java 7, so we
+ // can't use them here.
+ Pattern p = Pattern.compile(
+ "(?:([a-z]{2,3}(?:-[a-z]{2,3})*))+(?:-[A-Z][a-z]{3})?(?:-([A-Z]{2}|[0-9]{3}))?(?:-([a-zA-Z0-9]{5,8}|[0-9][a-zA-Z0-9]{3}))?(?:-.)*");
+
+ Matcher m = p.matcher(languageTag);
+
+ if (m.matches())
+ {
+ String language = m.group(1);
+ String region = m.group(2);
+ String variant = m.group(3);
+
+ try
+ {
+ region = getRegionAlpha2Code(Integer.parseInt(region));
+ }
+ catch (NumberFormatException e)
+ {
+ // ignore, alpha region code was supplied
+ }
+
+ // Language won't be null as the pattern requires it, but
+ // the region and variant might be.
+
+ if (region == null)
+ {
+ // There isn't a Locale constructor that allows a
+ // variant without a region, so don't bother checking
+ // variant for null here.
+
+ return new Locale(language);
+ }
+
+ if (variant == null)
+ {
+ return new Locale(language, region);
+ }
+
+ return new Locale(language, region, variant);
+ }
+
+ debug(String.format("Can't parse language tag: %s", languageTag));
+
+ // strip anything to a hyphen and try that
+ String[] split = languageTag.split("-", 1);
+
+ return new Locale(split[0]);
+ }
+
+ /**
+ * Gets all numerical information for the given locale. If the
+ * given locale tag is null or empty, the default locale is used. The
+ * information is returned with each item grouped to make it
+ * easier to parse in TeX. This is an abridged version of
+ * getLocaleData().
+ * @param localeTag the tag identifying the locale or null for
+ * the default locale
+ * @return locale numerical information: language tag,
+ * number group separator, decimal separator, exponent separator,
+ * grouping conditional (1 if locale uses number grouping
+ * otherwise 0),
+ * currency code (e.g. GBP), regional currency identifier (e.g. IMP),
+ * currency symbol (e.g. \\twrp{&0x00A3;}), currency TeX code (e.g.
+ * \\texosquerycurrency{pound}), monetary decimal separator.
+ * @since 1.2
+ */
+ public String getNumericalInfo(String localeTag)
+ {
+ Locale locale;
+
+ if (localeTag == null || "".equals(localeTag))
+ {
+ locale = Locale.getDefault();
+ }
+ else
+ {
+ locale = getLocale(localeTag);
+ }
+
+ DecimalFormatSymbols fmtSyms
+ = DecimalFormatSymbols.getInstance(locale);
+
+ // ISO 4217 code
+ String currencyCode = fmtSyms.getInternationalCurrencySymbol();
+
+ // Currency symbol
+ String currency = fmtSyms.getCurrencySymbol();
+
+ // Check for known unofficial currency codes
+
+ String localeCurrencyCode = currencyCode;
+
+ String countryCode = locale.getCountry();
+
+ if (countryCode != null && !"".equals(countryCode))
+ {
+ if (countryCode.equals("GG") || countryCode.equals("GGY")
+ || countryCode.equals("831"))
+ {// Guernsey
+ localeCurrencyCode = "GGP";
+ currency = POUND_STRING;
+ }
+ else if (countryCode.equals("JE") || countryCode.equals("JEY")
+ || countryCode.equals("832"))
+ {// Jersey
+ localeCurrencyCode = "JEP";
+ currency = POUND_STRING;
+ }
+ else if (countryCode.equals("IM") || countryCode.equals("IMN")
+ || countryCode.equals("833"))
+ {// Isle of Man
+ localeCurrencyCode = "IMP";
+ currency = String.format("M%s", POUND_STRING);
+ }
+ else if (countryCode.equals("KI") || countryCode.equals("KIR")
+ || countryCode.equals("296"))
+ {// Kiribati
+ localeCurrencyCode = "KID";
+ currency = "$";
+ }
+ else if (countryCode.equals("TV") || countryCode.equals("TUV")
+ || countryCode.equals("798"))
+ {// Tuvaluan
+ localeCurrencyCode = "TVD";
+ currency = "$";
+ }
+ // Transnistrian ruble omitted as it conflicts with ISO
+ // 4217 so omitted. There's also no country code for
+ // Transnistria. Other currencies don't have an associated
+ // region code (for example, Somaliland) or don't have a
+ // known unofficial currency code (for example, Alderney).
+ }
+
+ // Convert known Unicode currency symbols to commands that
+ // may be redefined in TeX
+
+ String texCurrency = getTeXCurrency(currency);
+
+ NumberFormat numFormat = NumberFormat.getNumberInstance(locale);
+
+ // Currency codes should always be three letter upper case
+ // A-Z characters, so no need to escape them.
+
+ return String.format(
+ "{%s}{%s}{%s}{%s}{%d}{%s}{%s}{%s}{%s}{%s}",
+ escapeFileName(getLanguageTag(locale)),
+ escapeText(fmtSyms.getGroupingSeparator()),
+ escapeText(fmtSyms.getDecimalSeparator()),
+ escapeText(fmtSyms.getExponentSeparator()),
+ numFormat.isGroupingUsed() ? 1 : 0,
+ currencyCode,
+ localeCurrencyCode,
+ escapeText(currency),
+ texCurrency,// already escaped
+ escapeText(fmtSyms.getMonetaryDecimalSeparator()));
+ }
+
+ /**
+ * Gets the currency with known symbols replaced by TeX commands
+ * provided by texosquery.tex. Some of the conditions in this
+ * method test for archaic currency symbols. It seems very
+ * unlikely that any of those cases would actually occur, but
+ * they're included for completeness.
+ * @param currency The original currency string
+ * @return The TeX version
+ * @since 1.2
+ */
+ public String getTeXCurrency(String currency)
+ {
+ StringBuilder builder = new StringBuilder();
+
+ for (int i = 0, n = currency.length(); i < n; )
+ {
+ int codepoint = currency.codePointAt(i);
+ i += Character.charCount(codepoint);
+
+ switch (codepoint)
+ {
+ case DOLLAR_CHAR:
+ builder.append("\\texosquerycurrency{dollar}");
+ break;
+ case CENT_CHAR:
+ builder.append("\\texosquerycurrency{cent}");
+ break;
+ case POUND_CHAR:
+ builder.append("\\texosquerycurrency{pound}");
+ break;
+ case CURRENCY_CHAR:
+ builder.append("\\texosquerycurrency{sign}");
+ break;
+ case YEN_CHAR:
+ builder.append("\\texosquerycurrency{yen}");
+ break;
+ case ECU_CHAR:
+ builder.append("\\texosquerycurrency{ecu}");
+ break;
+ case COLON_CURRENCY_CHAR:
+ builder.append("\\texosquerycurrency{colon}");
+ break;
+ case CRUZEIRO_CHAR:
+ builder.append("\\texosquerycurrency{cruzeiro}");
+ break;
+ case FRANC_CHAR:
+ builder.append("\\texosquerycurrency{franc}");
+ break;
+ case LIRA_CHAR:
+ builder.append("\\texosquerycurrency{lira}");
+ break;
+ case MILL_CURRENCY_CHAR:
+ builder.append("\\texosquerycurrency{mill}");
+ break;
+ case NAIRA_CHAR:
+ builder.append("\\texosquerycurrency{naira}");
+ break;
+ case PESETA_CHAR:
+ builder.append("\\texosquerycurrency{peseta}");
+ break;
+ case LEGACY_RUPEE_CHAR:
+ case RUPEE_CHAR:
+ builder.append("\\texosquerycurrency{rupee}");
+ break;
+ case WON_CHAR:
+ builder.append("\\texosquerycurrency{won}");
+ break;
+ case NEW_SHEQEL_CHAR:
+ builder.append("\\texosquerycurrency{newsheqel}");
+ break;
+ case DONG_CHAR:
+ builder.append("\\texosquerycurrency{dong}");
+ break;
+ case EURO_CHAR:
+ builder.append("\\texosquerycurrency{euro}");
+ break;
+ case KIP_CHAR:
+ builder.append("\\texosquerycurrency{kip}");
+ break;
+ case TUGRIK_CHAR:
+ builder.append("\\texosquerycurrency{tugrik}");
+ break;
+ case DRACHMA_CHAR:
+ builder.append("\\texosquerycurrency{drachma}");
+ break;
+ case GERMAN_PENNY_CHAR:
+ builder.append("\\texosquerycurrency{germanpenny}");
+ break;
+ case PESO_CHAR:
+ builder.append("\\texosquerycurrency{peso}");
+ break;
+ case GUARANI_CHAR:
+ builder.append("\\texosquerycurrency{guarani}");
+ break;
+ case AUSTRAL_CHAR:
+ builder.append("\\texosquerycurrency{austral}");
+ break;
+ case HRYVNIA_CHAR:
+ builder.append("\\texosquerycurrency{hryvnia}");
+ break;
+ case CEDI_CHAR:
+ builder.append("\\texosquerycurrency{cedi}");
+ break;
+ case LIVRE_TOURNOIS_CHAR:
+ builder.append("\\texosquerycurrency{livretournois}");
+ break;
+ case SPESMILO_CHAR:
+ builder.append("\\texosquerycurrency{spesmilo}");
+ break;
+ case TENGE_CHAR:
+ builder.append("\\texosquerycurrency{tenge}");
+ break;
+ case TURKISH_LIRA_CHAR:
+ builder.append("\\texosquerycurrency{turkishlira}");
+ break;
+ case NORDIC_MARK_CHAR:
+ builder.append("\\texosquerycurrency{nordicmark}");
+ break;
+ case MANAT_CHAR:
+ builder.append("\\texosquerycurrency{manat}");
+ break;
+ case RUBLE_CHAR:
+ builder.append("\\texosquerycurrency{ruble}");
+ break;
+ default:
+ builder.append(escapeText(codepoint));
+ }
+ }
+
+ return builder.toString();
+ }
+
+ /** Gets the standalone month names for the locale data.
+ * These are only available for Java 8, so just return the
+ * month names used in the date format instead. The JRE8 version
+ * needs to override this method.
+ * @param cal The calendar
+ * @param locale The locale
+ * @return month names
+ * @since 1.2
+ */
+ public String getStandaloneMonths(Calendar cal, Locale locale)
+ {
+ // can't use Calendar.getDisplayName() as it's not available
+ // with Java 5.
+ DateFormatSymbols dateFmtSyms = DateFormatSymbols.getInstance(locale);
+
+ StringBuilder monthNamesGroup = new StringBuilder();
+
+ String[] names = dateFmtSyms.getMonths();
+
+ for (int i = 0; i < 12; i++)
+ {
+ monthNamesGroup.append(String.format("{%s}", escapeText(names[i])));
+ }
+
+ return monthNamesGroup.toString();
+ }
+
+ /** Gets the standalone short month names for the locale data.
+ * These are only available for Java 8, so just return the
+ * month names used in the date format instead. The JRE8 version
+ * needs to override this method.
+ * @param cal The calendar
+ * @param locale The locale
+ * @return short month names
+ * @since 1.2
+ */
+ public String getStandaloneShortMonths(Calendar cal, Locale locale)
+ {
+ // can't use Calendar.getDisplayName() as it's not available
+ // with Java 5.
+ DateFormatSymbols dateFmtSyms = DateFormatSymbols.getInstance(locale);
+
+ StringBuilder shortMonthNamesGroup = new StringBuilder();
+
+ String[] names = dateFmtSyms.getShortMonths();
+
+ for (int i = 0; i < 12; i++)
+ {
+ shortMonthNamesGroup.append(String.format("{%s}",
+ escapeText(names[i])));
+ }
+
+ return shortMonthNamesGroup.toString();
+ }
+
+ /** Gets the standalone day names for the locale data.
+ * These are only available for Java 8, so just return the
+ * names used in the date format instead. The JRE8 version
+ * needs to override this method.
+ * @param cal The calendar
+ * @param locale The locale
+ * @return day of week names
+ * @since 1.2
+ */
+ public String getStandaloneWeekdays(Calendar cal, Locale locale)
+ {
+ DateFormatSymbols dateFmtSyms = DateFormatSymbols.getInstance(locale);
+
+ String[] names = dateFmtSyms.getWeekdays();
+
+ // Be consistent with pgfcalendar:
+
+ return String.format("{%s}{%s}{%s}{%s}{%s}{%s}{%s}",
+ escapeText(names[Calendar.MONDAY]),
+ escapeText(names[Calendar.TUESDAY]),
+ escapeText(names[Calendar.WEDNESDAY]),
+ escapeText(names[Calendar.THURSDAY]),
+ escapeText(names[Calendar.FRIDAY]),
+ escapeText(names[Calendar.SATURDAY]),
+ escapeText(names[Calendar.SUNDAY]));
+ }
+
+ /** Gets the standalone short day names for the locale data.
+ * These are only available for Java 8, so just return the
+ * names used in the date format instead. The JRE8 version
+ * needs to override this method.
+ * @param cal The calendar
+ * @param locale The locale
+ * @return day of week names
+ * @since 1.2
+ */
+ public String getStandaloneShortWeekdays(Calendar cal, Locale locale)
+ {
+ DateFormatSymbols dateFmtSyms = DateFormatSymbols.getInstance(locale);
+
+ String[] names = dateFmtSyms.getShortWeekdays();
+
+ // Be consistent with pgfcalendar:
+
+ return String.format("{%s}{%s}{%s}{%s}{%s}{%s}{%s}",
+ escapeText(names[Calendar.MONDAY]),
+ escapeText(names[Calendar.TUESDAY]),
+ escapeText(names[Calendar.WEDNESDAY]),
+ escapeText(names[Calendar.THURSDAY]),
+ escapeText(names[Calendar.FRIDAY]),
+ escapeText(names[Calendar.SATURDAY]),
+ escapeText(names[Calendar.SUNDAY]));
+ }
+
+ /**
+ * Converts date/time pattern to a form that's easier for TeX to
+ * parse. This replaces the placeholders with <tt>\\patdtf{n}{c}</tt> where c
+ * is the placeholder character and n is the number of
+ * occurrences of c in the placeholder. (For example,
+ * "<tt>dd-MMM-yyyy</tt>" is converted to
+ * <tt>\\patdtf{2}{d}-\\patdtf{3}{M}-\\patdtf{4}{y}</tt>). The
+ * query command \\TeXOSQuery in texosquery.tex will expand \\patdtf
+ * to the longer \\texosquerydtf to avoid conflict. This can then be
+ * redefined as appropriate. (See the "Pattern Formats" section
+ * of the TeX documented code for more detail.)
+ * @param localeFormat The date/time pattern
+ * @return TeX code
+ * @since 1.2
+ */
+ public String formatDateTimePattern(Format localeFormat)
+ {
+ SimpleDateFormat fmt = null;
+
+ try
+ {
+ fmt = (SimpleDateFormat)localeFormat;
+
+ if (fmt == null)
+ {
+ throw new NullPointerException();
+ }
+ }
+ catch (Exception e)
+ {
+ // this shouldn't happen
+ debug(String.format("Invalid argument: %s", localeFormat), e);
+ return "";
+ }
+
+ String pattern = fmt.toPattern();
+
+ StringBuilder builder = new StringBuilder();
+
+ int prev = 0;
+ int fieldLen = 0;
+ boolean inString = false;
+
+ for (int i = 0, n = pattern.length(), offset=1; i < n; i = i+offset)
+ {
+ int codepoint = pattern.codePointAt(i);
+ offset = Character.charCount(codepoint);
+
+ int nextIndex = i+offset;
+ int nextCodePoint = (nextIndex < n ? pattern.codePointAt(nextIndex):0);
+
+ if (inString)
+ {
+ if (codepoint == '\'')
+ {
+ if (nextCodePoint != '\'')
+ {
+ // reached the end of the string
+ builder.append('}');
+ inString = false;
+ }
+ else
+ {
+ // literal '
+ builder.append("\\patapo ");
+ i = nextIndex;
+ offset = Character.charCount(nextCodePoint);
+ }
+ }
+ else
+ {
+ // still inside the string
+ builder.append(escapeText(codepoint));
+ }
+ }
+ else if (codepoint == prev)
+ {
+ fieldLen++;
+ }
+ else
+ {
+ switch (codepoint)
+ {
+ case '\'': // quote
+
+ if (prev != 0)
+ {
+ builder.append(String.format(
+ "\\patdtf{%d}{%c}", fieldLen, prev));
+ prev = 0;
+ fieldLen = 0;
+ }
+
+ // start of the string
+ builder.append("\\patstr{");
+ inString = true;
+
+ break;
+ case 'G': // era
+ case 'y': // year
+ case 'Y': // week year
+ case 'M': // month in year (context sensitive)
+ case 'L': // month in year (standalone)
+ case 'w': // week in year
+ case 'W': // week in month
+ case 'D': // day in year
+ case 'd': // day in month
+ case 'F': // day of week in month
+ case 'E': // day name in week
+ case 'u': // day number of week (1 = Mon)
+ case 'a': // am/pm marker
+ case 'H': // hour in day (0-23)
+ case 'k': // hour in day (1-24)
+ case 'K': // hour in am/pm (0-11)
+ case 'h': // hour in am/pm (1-12)
+ case 'm': // minute in hour
+ case 's': // second in minute
+ case 'S': // millisecond
+ case 'z': // time zone (locale)
+ case 'Z': // time zone (RFC 822)
+ case 'X': // time zone (ISO 8601)
+ prev = codepoint;
+ fieldLen = 1;
+ break;
+ default:
+ // prev doesn't need escaping as it will be one
+ // of the above letter cases.
+
+ if (prev == 0)
+ {
+ builder.append(escapeText(codepoint));
+ }
+ else
+ {
+ builder.append(String.format(
+ "\\patdtf{%d}{%c}%s", fieldLen, prev,
+ escapeText(codepoint)));
+ }
+ prev = 0;
+ fieldLen = 0;
+ }
+ }
+ }
+
+ if (prev != 0)
+ {
+ builder.append(String.format(
+ "\\patdtf{%d}{%c}", fieldLen, prev));
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Converts numeric pattern to a form that's easier for TeX to parse.
+ * @param numFormat the numeric pattern
+ * @return TeX code
+ * @since 1.2
+ */
+ public String formatNumberPattern(Format numFormat)
+ {
+ DecimalFormat fmt = null;
+
+ try
+ {
+ fmt = (DecimalFormat)numFormat;
+
+ if (fmt == null)
+ {
+ throw new NullPointerException();
+ }
+ }
+ catch (Exception e)
+ {
+ // this shouldn't happen
+ debug(String.format("Invalid argument: %s", numFormat), e);
+ return "";
+ }
+
+ String pattern = fmt.toPattern();
+
+ // Is there a +ve;-ve sub-pattern pair?
+ // This is a bit awkward as a semi-colon could appear
+ // literally within a string.
+
+ String positive = null;
+ String negative = null;
+
+ StringBuilder builder = new StringBuilder();
+ boolean inString = false;
+
+ for (int i = 0, n = pattern.length(), offset=1; i < n; i = i+offset)
+ {
+ int codepoint = pattern.codePointAt(i);
+ offset = Character.charCount(codepoint);
+
+ int nextIndex = i+offset;
+ int nextCodePoint = (nextIndex < n ? pattern.codePointAt(nextIndex):0);
+
+ if (inString)
+ {
+ if (codepoint == '\'')
+ {
+ builder.appendCodePoint(codepoint);
+
+ if (nextCodePoint == '\'')
+ {
+ // literal '
+ builder.appendCodePoint(nextCodePoint);
+ i = nextIndex;
+ offset = Character.charCount(nextCodePoint);
+ }
+ else
+ {
+ inString = false;
+ }
+ }
+ else
+ {
+ builder.appendCodePoint(codepoint);
+ }
+ }
+ else if (codepoint == '\'')
+ {
+ inString = true;
+ builder.appendCodePoint(codepoint);
+ }
+ else if (codepoint == ';')
+ {
+ if (positive == null)
+ {
+ positive = builder.toString();
+ builder = new StringBuilder();
+ }
+ else
+ {
+ debug(String.format("Too many ; found in pattern: %s",
+ pattern));
+ }
+ }
+ else
+ {
+ builder.appendCodePoint(codepoint);
+ }
+ }
+
+ if (positive == null)
+ {
+ positive = builder.toString();
+ }
+ else if (builder.length() > 0)
+ {
+ negative = builder.toString();
+ }
+
+ if (negative == null)
+ {
+ return String.format("\\patnumfmt{%s}",
+ formatNumberSubPattern(positive));
+ }
+ else
+ {
+ return String.format("\\patpmnumfmt{%s}{%s}",
+ formatNumberSubPattern(positive),
+ formatNumberSubPattern(negative));
+ }
+ }
+
+ /**
+ * Converts the sub-pattern of a numeric format.
+ * @param pattern The sub-pattern
+ * @return TeX code
+ * @since 1.2
+ */
+ private String formatNumberSubPattern(String pattern)
+ {
+ if (pattern == null || "".equals(pattern))
+ {
+ return "";
+ }
+
+ // Is this currency?
+
+ Pattern p = Pattern.compile("(.*(?:[^'](?:'')+){0,1})("+CURRENCY_CHAR
+ +"{1,2})(.*)");
+ Matcher m = p.matcher(pattern);
+
+ if (m.matches())
+ {
+ return formatCurrencyPattern(m.group(1),
+ (m.group(2).length() == 2), m.group(3));
+ }
+
+ // Is this a percentage?
+
+ p = Pattern.compile("(.*(?:[^'](?:'')+){0,1})([%"+PERMILLE_CHAR+"])(.*)");
+ m = p.matcher(pattern);
+
+ if (m.matches())
+ {
+ boolean percent = ("%".equals(m.group(2)));
+
+ return formatPercentagePattern(m.group(1), m.group(3),
+ percent ? "patppct" : "patppml",
+ percent ? "patspct" : "patspml");
+ }
+
+ // must be a number
+
+ return formatNumericPattern(pattern);
+ }
+
+ /**
+ * Converts the currency format.
+ * @param pre The pre-symbol pattern
+ * @param international Determines if the international currency
+ * symbol should be used
+ * @param post The post-symbol pattern
+ * @return TeX code
+ * @since 1.2
+ */
+ private String formatCurrencyPattern(String pre, boolean international,
+ String post)
+ {
+ if (post == null || "".equals(post))
+ {
+ pre = formatNumericPattern(pre);
+
+ // currency symbol is a suffix
+ if (international)
+ {
+ return String.format("\\patsicur{%s}{}", pre);
+ }
+ else
+ {
+ return String.format("\\patscur{%s}{}", pre);
+ }
+ }
+ else if (pre == null || "".equals(pre))
+ {
+ // currency symbol is a prefix
+
+ post = formatNumericPattern(post);
+
+ if (international)
+ {
+ return String.format("\\patpicur{%s}{}", post);
+ }
+ else
+ {
+ return String.format("\\patpcur{%s}{}", post);
+ }
+ }
+ else
+ {
+ // What do we do here? If pre contains '#' or '0' assume
+ // a suffix currency otherwise a prefix currency.
+
+ pre = formatNumericPattern(pre);
+ post = formatNumericPattern(post);
+
+ if (pre.matches(".*[0#].*"))
+ {
+ // suffix, pre is the number and post is trailing
+ // text
+ if (international)
+ {
+ return String.format("\\patsicur{%s}{%s}", pre, post);
+ }
+ else
+ {
+ return String.format("\\patscur{%s}{%s}", pre, post);
+ }
+ }
+ else
+ {
+ // prefix, post is the number and pre is leading
+ // text
+ if (international)
+ {
+ return String.format("\\patpicur{%s}{%s}", post, pre);
+ }
+ else
+ {
+ return String.format("\\patpcur{%s}{%s}", post, pre);
+ }
+ }
+ }
+ }
+
+ /**
+ * Converts percentage format.
+ * @param pre The pre-symbol pattern
+ * @param post The post-symbol pattern
+ * @param prefixCs The control sequence name to use if the symbol is a
+ * prefix
+ * @param suffixCs The control sequence name to use if the symbol is a
+ * suffix
+ * @return TeX code
+ * @since 1.2
+ */
+ private String formatPercentagePattern(String pre, String post,
+ String prefixCs, String suffixCs)
+ {
+ if (post == null || "".equals(post))
+ {
+ pre = formatNumericPattern(pre);
+
+ // symbol is a suffix
+
+ return String.format("\\%s{%s}{}", suffixCs, pre);
+ }
+ else if (pre == null || "".equals(pre))
+ {
+ // symbol is a prefix
+
+ post = formatNumericPattern(post);
+
+ return String.format("\\%s{%s}{}", prefixCs, post);
+ }
+ else
+ {
+ pre = formatNumericPattern(pre);
+ post = formatNumericPattern(post);
+
+ if (pre.matches(".*[0#].*"))
+ {
+ // suffix, pre is the number and post is trailing
+ // text
+
+ return String.format("\\%s{%s}{%s}", suffixCs, pre, post);
+ }
+ else
+ {
+ // prefix, post is the number and pre is leading
+ // text
+
+ return String.format("\\%s{%s}{%s}", prefixCs, post, pre);
+ }
+ }
+ }
+
+ /**
+ * Converts the numeric format.
+ * @param pattern The sub-pattern
+ * @return TeX code
+ * @since 1.2
+ */
+ private String formatNumericPattern(String pattern)
+ {
+ if (pattern == null || "".equals(pattern))
+ {
+ return "";
+ }
+
+ // Split around exponent (if present)
+
+ Pattern p = Pattern.compile("(.*(?:[^'](?:'')+?){0,1})(E.*)?");
+ Matcher m = p.matcher(pattern);
+
+ if (!m.matches())
+ {
+ debug(String.format(
+ "Can't match number format sub-pattern '%s' against regexp: %s",
+ pattern, p));
+ return "";
+ }
+
+ String pre = m.group(1);
+ String post = m.group(2);
+
+ if (pre == null && post == null)
+ {
+ // empty pattern
+ return "";
+ }
+
+ if (post == null)
+ {
+ return formatDecimalPattern(pre);
+ }
+
+ return String.format("\\patsinumfmt{%s}{%s}",
+ formatDecimalPattern(pre),
+ formatIntegerPattern(post, true));
+ }
+
+ /**
+ * Converts a decimal pattern.
+ * @param pattern The pattern
+ * @return TeX code
+ * @since 1.2
+ */
+ private String formatDecimalPattern(String pattern)
+ {
+ // split on the decimal point (if present)
+
+ Pattern p = Pattern.compile("(.*?(?:[^'](?:'')){0,1})(?:\\.(.*))?");
+
+ Matcher m = p.matcher(pattern);
+
+ if (!m.matches())
+ {
+ debug(String.format(
+ "Can't match decimal pattern '%s' against regexp: %s",
+ pattern, p));
+ return "";
+ }
+
+
+ String pre = m.group(1);
+ String post = m.group(2);
+
+ if (pre == null && post == null)
+ {
+ // empty pattern
+ return "";
+ }
+
+ if (post == null)
+ {
+ return formatIntegerPattern(pre, true);
+ }
+
+ return String.format("\\patdecfmt{%s}{%s}",
+ formatIntegerPattern(pre, true),
+ formatIntegerPattern(post, false));
+ }
+
+
+ /**
+ * Convert an integer pattern. The aim here is to have a number
+ * formatting command defined in TeX that will be passed a number
+ * with either leading or trailing zeros padded to 10 digits.
+ * TeX can't handle numbers higher than 2147483647, so any digits
+ * in the pattern beyond that are discarded. This means defining
+ * a command that effectively takes 10 arguments (with a bit of
+ * trickery to get around the 9-arg maximum). Each digit can then
+ * be rendered using either \\patdgt (always display the digit)
+ * or \\patdgtnz (only display the digit if it isn't a
+ * non-significant zero).
+ *
+ * These short commands will be converted to longer ones that are
+ * less likely to cause conflict when \\TeXOSQuery is used.
+ * (See the "Pattern Formats" section of the documented code for
+ * more details.)
+ * @param pattern The pattern
+ * @param leadPadding Determines if leading padding needs taking
+ * into account
+ * @return TeX code
+ * @since 1.2
+ */
+ private String formatIntegerPattern(String pattern, boolean leadPadding)
+ {
+ boolean inString = false;
+
+ int digitCount = 0;
+ int groupCount = -1;
+
+ // count the number of digits
+
+ for (int i = 0, n = pattern.length(), offset=1; i < n; i = i+offset)
+ {
+ int codepoint = pattern.codePointAt(i);
+ offset = Character.charCount(codepoint);
+
+ int nextIndex = i+offset;
+ int nextCodePoint = (nextIndex < n ? pattern.codePointAt(nextIndex):0);
+
+ if (inString)
+ {
+ if (codepoint == '\'')
+ {
+ if (nextCodePoint != '\'')
+ {
+ inString = false;
+ i = nextIndex;
+ offset = Character.charCount(nextCodePoint);
+ }
+ }
+ }
+ else if (codepoint == '\'')
+ {
+ inString = true;
+ }
+ else if (codepoint == '#' || codepoint == '0')
+ {
+ digitCount++;
+
+ if (groupCount > -1) groupCount++;
+ }
+ else if (codepoint == ',')
+ {
+ groupCount=0;
+ }
+ }
+
+ int digitIndex = (leadPadding ? MAX_DIGIT_FORMAT : 0);
+
+ inString = false;
+
+ StringBuilder builder = new StringBuilder();
+
+ for (int i = 0, n = pattern.length(), offset=1; i < n; i = i+offset)
+ {
+ int codepoint = pattern.codePointAt(i);
+ offset = Character.charCount(codepoint);
+
+ int nextIndex = i+offset;
+ int nextCodePoint = (nextIndex < n ? pattern.codePointAt(nextIndex):0);
+
+ switch (codepoint)
+ {
+ case '\'':
+
+ if (!inString)
+ {
+ inString = true;
+
+ builder.append("\\patstr{");
+ }
+ else if (nextCodePoint == '\'')
+ {
+ builder.append("\\patapo ");
+ i = nextIndex;
+ offset = Character.charCount(nextCodePoint);
+ }
+ else
+ {
+ builder.append("}");
+ inString = false;
+ }
+ break;
+ case '0':
+
+ if (leadPadding)
+ {
+ if (digitIndex > MAX_DIGIT_FORMAT)
+ {
+ // too many digit markers in the pattern,
+ // discard
+ }
+ else if (digitIndex > digitCount)
+ {
+ // not enough digit markers in the pattern
+ // pad with #
+
+ for ( ; digitIndex > digitCount; digitIndex--)
+ {
+ builder.append("\\patdgtnz ");
+
+ if (groupCount > 0 && ((digitIndex-1) % groupCount) == 0)
+ {
+ builder.append("\\patngp ");
+ }
+ }
+
+ builder.append("\\patdgt ");
+ }
+ else
+ {
+ builder.append("\\patdgt ");
+ }
+
+ digitIndex--;
+ }
+ else
+ {
+ digitIndex++;
+
+ if (digitIndex > MAX_DIGIT_FORMAT)
+ {
+ // too many digit markers in the pattern,
+ // discard
+ }
+ else if (digitIndex == digitCount)
+ {
+ builder.append("\\patdgt ");
+
+ // not enough digit markers in the pattern
+ // pad with #
+
+ for ( ; digitIndex < MAX_DIGIT_FORMAT; digitIndex++)
+ {
+ builder.append("\\patdgtnz ");
+ }
+ }
+ else
+ {
+ builder.append("\\patdgt ");
+ }
+ }
+ break;
+ case '#':
+
+ if (leadPadding)
+ {
+ if (digitIndex > MAX_DIGIT_FORMAT)
+ {
+ // too many digit markers in the pattern,
+ // discard
+ }
+ else if (digitIndex > digitCount)
+ {
+ // not enough digit markers in the pattern
+ // pad with #
+
+ for ( ; digitIndex > digitCount; digitIndex--)
+ {
+ builder.append("\\patdgtnz ");
+
+ if (groupCount > 0 && ((digitIndex-1) % groupCount) == 0)
+ {
+ builder.append("\\patngp ");
+ }
+ }
+
+ builder.append("\\patdgtnz ");
+ }
+ else
+ {
+ builder.append("\\patdgtnz ");
+ }
+
+ digitIndex--;
+ }
+ else
+ {
+ digitIndex++;
+
+ if (digitIndex > MAX_DIGIT_FORMAT)
+ {
+ // too many digit markers in the pattern,
+ // discard
+ }
+ else if (digitIndex == digitCount)
+ {
+ builder.append("\\patdgtnz ");
+
+ // not enough digit markers in the pattern
+ // pad with #
+
+ for ( ; digitIndex < MAX_DIGIT_FORMAT; digitIndex++)
+ {
+ builder.append("\\patdgtnz ");
+ }
+ }
+ else
+ {
+ builder.append("\\patdgtnz ");
+ }
+ }
+ break;
+ case '-':
+ builder.append("\\patmsg ");
+ break;
+ case ',':
+
+ if (digitIndex <= digitCount)
+ {
+ builder.append("\\patngp ");
+ }
+
+ break;
+ default:
+ builder.append(escapeText(codepoint));
+ }
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Gets all available data for the given locale. If the
+ * given locale tag is null, the default locale is used. The
+ * information is returned with grouping to make it
+ * easier to parse in TeX. (Each block is grouped, with each
+ * element within the block also grouped.)
+ *
+ * The standalone month names and day of week names are new
+ * to Java 8, so we can't use them for earlier versions.
+ * @param localeTag the language tag identifying the locale or null for
+ * the default locale
+ * @return locale data in grouped blocks:
+ * <ol>
+ * <li>language tag, language name, language name in given locale,
+ * country name, country name in given locale, variant name,
+ * variant name in given locale.
+ * <li> full date, long date, medium date, short date,
+ * first day of the week index.
+ * <li> full date, long date, medium date, short date patterns.
+ * <li> full time, long time, medium time, short time.
+ * <li> full time, long time, medium time, short time patterns.
+ * <li> weekday names.
+ * <li> short weekday names.
+ * <li> month names
+ * <li> short month names.
+ * <li> standalone week day names.
+ * <li> standalone short week day names.
+ * <li> standalone month names.
+ * <li> standalone short month names.
+ * <li> number group separator,
+ * decimal separator, exponent separator, grouping flag, ISO 4217 currency
+ * identifier (e.g. GBP), region currency identifier (usually the same as
+ * the ISO 4217 code, but may be an unofficial currency code, such as IMP),
+ * currency symbol (e.g. &0x00A3;), TeX currency symbol, monetary decimal
+ * separator, percent symbol, per mille symbol.
+ * <li> number format, integer format, currency format,
+ * percent format.
+ * </ol>
+ * @since 1.2
+ */
+ public String getLocaleData(String localeTag)
+ {
+ Locale locale;
+
+ if (localeTag == null || "".equals(localeTag))
+ {
+ locale = Locale.getDefault();
+ }
+ else
+ {
+ locale = getLocale(localeTag);
+ }
+
+ String languageName = locale.getDisplayLanguage();
+
+ if (languageName == null)
+ {
+ languageName = "";
+ }
+
+ String localeLanguageName = locale.getDisplayLanguage(locale);
+
+ if (localeLanguageName == null)
+ {
+ localeLanguageName = "";
+ }
+
+ String countryName = locale.getDisplayCountry();
+
+ if (countryName == null)
+ {
+ countryName = "";
+ }
+
+ String localeCountryName = locale.getDisplayCountry(locale);
+
+ if (localeCountryName == null)
+ {
+ localeCountryName = "";
+ }
+
+ String variantName = locale.getDisplayVariant();
+
+ if (variantName == null)
+ {
+ variantName = "";
+ }
+
+ String localeVariantName = locale.getDisplayVariant(locale);
+
+ if (localeVariantName == null)
+ {
+ localeVariantName = "";
+ }
+
+ String langRegionGroup = String.format("{%s}{%s}{%s}{%s}{%s}{%s}{%s}",
+ escapeFileName(getLanguageTag(locale)),
+ escapeText(languageName),
+ escapeText(localeLanguageName),
+ escapeText(countryName),
+ escapeText(localeCountryName),
+ escapeText(variantName),
+ escapeText(localeVariantName));
+
+ DateFormat dateFullFormat = DateFormat.getDateInstance(
+ DateFormat.FULL, locale);
+
+ DateFormat dateLongFormat = DateFormat.getDateInstance(
+ DateFormat.LONG, locale);
+
+ DateFormat dateMediumFormat = DateFormat.getDateInstance(
+ DateFormat.MEDIUM, locale);
+
+ DateFormat dateShortFormat = DateFormat.getDateInstance(
+ DateFormat.SHORT, locale);
+
+ DateFormat timeFullFormat = DateFormat.getTimeInstance(
+ DateFormat.FULL, locale);
+
+ DateFormat timeLongFormat = DateFormat.getTimeInstance(
+ DateFormat.LONG, locale);
+
+ DateFormat timeMediumFormat = DateFormat.getTimeInstance(
+ DateFormat.MEDIUM, locale);
+
+ DateFormat timeShortFormat = DateFormat.getTimeInstance(
+ DateFormat.SHORT, locale);
+
+ DateFormat dateTimeFullFormat = DateFormat.getDateTimeInstance(
+ DateFormat.FULL, DateFormat.FULL, locale);
+
+ DateFormat dateTimeLongFormat = DateFormat.getDateTimeInstance(
+ DateFormat.LONG, DateFormat.LONG, locale);
+
+ DateFormat dateTimeMediumFormat = DateFormat.getDateTimeInstance(
+ DateFormat.MEDIUM, DateFormat.MEDIUM, locale);
+
+ DateFormat dateTimeShortFormat = DateFormat.getDateTimeInstance(
+ DateFormat.SHORT, DateFormat.SHORT, locale);
+
+ // first day of the week index consistent with pgfcalendar
+ // (0 = Monday, etc)
+ int firstDay = 0;
+
+ Calendar cal = Calendar.getInstance(locale);
+ cal.setTimeInMillis(now.getTime());
+
+ switch (cal.getFirstDayOfWeek())
+ {
+ case Calendar.MONDAY:
+ firstDay = 0;
+ break;
+ case Calendar.TUESDAY:
+ firstDay = 1;
+ break;
+ case Calendar.WEDNESDAY:
+ firstDay = 2;
+ break;
+ case Calendar.THURSDAY:
+ firstDay = 3;
+ break;
+ case Calendar.FRIDAY:
+ firstDay = 4;
+ break;
+ case Calendar.SATURDAY:
+ firstDay = 5;
+ break;
+ case Calendar.SUNDAY:
+ firstDay = 6;
+ break;
+ }
+
+ String dateGroup = String.format("{%s}{%s}{%s}{%s}{%d}",
+ escapeText(dateFullFormat.format(now)),
+ escapeText(dateLongFormat.format(now)),
+ escapeText(dateMediumFormat.format(now)),
+ escapeText(dateShortFormat.format(now)),
+ firstDay);
+
+ String dateFmtGroup = String.format("{%s}{%s}{%s}{%s}",
+ formatDateTimePattern(dateFullFormat),
+ formatDateTimePattern(dateLongFormat),
+ formatDateTimePattern(dateMediumFormat),
+ formatDateTimePattern(dateShortFormat));
+
+ String timeGroup = String.format("{%s}{%s}{%s}{%s}",
+ escapeText(timeFullFormat.format(now)),
+ escapeText(timeLongFormat.format(now)),
+ escapeText(timeMediumFormat.format(now)),
+ escapeText(timeShortFormat.format(now)));
+
+ String timeFmtGroup = String.format("{%s}{%s}{%s}{%s}",
+ formatDateTimePattern(timeFullFormat),
+ formatDateTimePattern(timeLongFormat),
+ formatDateTimePattern(timeMediumFormat),
+ formatDateTimePattern(timeShortFormat));
+
+ String dateTimeGroup = String.format("{%s}{%s}{%s}{%s}",
+ escapeText(dateTimeFullFormat.format(now)),
+ escapeText(dateTimeLongFormat.format(now)),
+ escapeText(dateTimeMediumFormat.format(now)),
+ escapeText(dateTimeShortFormat.format(now)));
+
+ String dateTimeFmtGroup = String.format("{%s}{%s}{%s}{%s}",
+ formatDateTimePattern(dateTimeFullFormat),
+ formatDateTimePattern(dateTimeLongFormat),
+ formatDateTimePattern(dateTimeMediumFormat),
+ formatDateTimePattern(dateTimeShortFormat));
+
+ DateFormatSymbols dateFmtSyms = DateFormatSymbols.getInstance(locale);
+
+ String[] names = dateFmtSyms.getWeekdays();
+
+ // Be consistent with pgfcalendar:
+
+ String weekdayNamesGroup = String.format(
+ "{%s}{%s}{%s}{%s}{%s}{%s}{%s}",
+ escapeText(names[Calendar.MONDAY]),
+ escapeText(names[Calendar.TUESDAY]),
+ escapeText(names[Calendar.WEDNESDAY]),
+ escapeText(names[Calendar.THURSDAY]),
+ escapeText(names[Calendar.FRIDAY]),
+ escapeText(names[Calendar.SATURDAY]),
+ escapeText(names[Calendar.SUNDAY]));
+
+ names = dateFmtSyms.getShortWeekdays();
+
+ String shortWeekdayNamesGroup = String.format(
+ "{%s}{%s}{%s}{%s}{%s}{%s}{%s}",
+ escapeText(names[Calendar.MONDAY]),
+ escapeText(names[Calendar.TUESDAY]),
+ escapeText(names[Calendar.WEDNESDAY]),
+ escapeText(names[Calendar.THURSDAY]),
+ escapeText(names[Calendar.FRIDAY]),
+ escapeText(names[Calendar.SATURDAY]),
+ escapeText(names[Calendar.SUNDAY]));
+
+ StringBuilder monthNamesGroup = new StringBuilder();
+
+ names = dateFmtSyms.getMonths();
+
+ for (int i = 0; i < 12; i++)
+ {
+ // skip 13th month (Calendar.UNDECIMBER)
+ monthNamesGroup.append(String.format("{%s}", escapeText(names[i])));
+ }
+
+ StringBuilder shortMonthNamesGroup = new StringBuilder();
+
+ names = dateFmtSyms.getShortMonths();
+
+ for (int i = 0; i < 12; i++)
+ {
+ shortMonthNamesGroup.append(String.format("{%s}",
+ escapeText(names[i])));
+ }
+
+ // Get numerical data (as with getNumericalInfo)
+ DecimalFormatSymbols fmtSyms
+ = DecimalFormatSymbols.getInstance(locale);
+
+ // ISO 4217 code
+ String currencyCode = fmtSyms.getInternationalCurrencySymbol();
+
+ // Currency symbol
+ String currency = fmtSyms.getCurrencySymbol();
+
+ // Check for known unofficial currency codes
+
+ String localeCurrencyCode = currencyCode;
+
+ String countryCode = locale.getCountry();
+
+ if (countryCode != null && !"".equals(countryCode))
+ {
+ if (countryCode.equals("GG") || countryCode.equals("GGY")
+ || countryCode.equals("831"))
+ {// Guernsey
+ localeCurrencyCode = "GGP";
+ currency = POUND_STRING;
+ }
+ else if (countryCode.equals("JE") || countryCode.equals("JEY")
+ || countryCode.equals("832"))
+ {// Jersey
+ localeCurrencyCode = "JEP";
+ currency = POUND_STRING;
+ }
+ else if (countryCode.equals("IM") || countryCode.equals("IMN")
+ || countryCode.equals("833"))
+ {// Isle of Man
+ localeCurrencyCode = "IMP";
+ currency = String.format("M%s", POUND_STRING);
+ }
+ else if (countryCode.equals("KI") || countryCode.equals("KIR")
+ || countryCode.equals("296"))
+ {// Kiribati
+ localeCurrencyCode = "KID";
+ currency = "$";
+ }
+ else if (countryCode.equals("TV") || countryCode.equals("TUV")
+ || countryCode.equals("798"))
+ {// Tuvaluan
+ localeCurrencyCode = "TVD";
+ currency = "$";
+ }
+ // Transnistrian ruble omitted as it conflicts with ISO
+ // 4217. There's also no country code for
+ // Transnistria. Other currencies don't have an associated
+ // region code (for example, Somaliland) or don't have a
+ // known unofficial currency code (for example, Alderney).
+ }
+
+ // Convert known Unicode currency symbols to commands that
+ // may be redefined in TeX
+
+ String texCurrency = getTeXCurrency(currency);
+
+ NumberFormat numFormat = NumberFormat.getNumberInstance(locale);
+ NumberFormat intFormat = NumberFormat.getIntegerInstance(locale);
+ NumberFormat curFormat = NumberFormat.getCurrencyInstance(locale);
+ NumberFormat pcFormat = NumberFormat.getPercentInstance(locale);
+
+ String numGroup = String.format(
+ "{%s}{%s}{%s}{%d}{%s}{%s}{%s}{%s}{%s}{%s}{%s}",
+ escapeText(fmtSyms.getGroupingSeparator()),
+ escapeText(fmtSyms.getDecimalSeparator()),
+ escapeText(fmtSyms.getExponentSeparator()),
+ numFormat.isGroupingUsed() ? 1 : 0,
+ currencyCode,
+ localeCurrencyCode,
+ escapeText(currency),
+ texCurrency,// already escaped
+ escapeText(fmtSyms.getMonetaryDecimalSeparator()),
+ escapeText(fmtSyms.getPercent()),
+ escapeText(fmtSyms.getPerMill()));
+
+ String numFmtGroup = String.format("{%s}{%s}{%s}{%s}",
+ formatNumberPattern(numFormat),
+ formatNumberPattern(intFormat),
+ formatNumberPattern(curFormat),
+ formatNumberPattern(pcFormat));
+
+ return String.format(
+ "{%s}{%s}{%s}{%s}{%s}{%s}{%s}{%s}{%s}{%s}{%s}{%s}{%s}{%s}{%s}{%s}{%s}",
+ langRegionGroup,
+ dateGroup,
+ dateFmtGroup,
+ timeGroup,
+ timeFmtGroup,
+ dateTimeGroup,
+ dateTimeFmtGroup,
+ weekdayNamesGroup,
+ shortWeekdayNamesGroup,
+ monthNamesGroup,
+ shortMonthNamesGroup,
+ getStandaloneWeekdays(cal, locale),
+ getStandaloneShortWeekdays(cal, locale),
+ getStandaloneMonths(cal, locale),
+ getStandaloneShortMonths(cal, locale),
+ numGroup, numFmtGroup);
+ }
+
+ /**
+ * Prints the syntax usage.
+ */
+ protected void syntax()
+ {
+ System.out.println(String.format("Usage: %s [<options>] <actions>", name));
+
+ System.out.println();
+ System.out.println("Cross-platform OS query application");
+ System.out.println("for use with TeX's shell escape.");
+ System.out.println();
+ System.out.println("Each query displays the result in a single line.");
+ System.out.println("An empty string is printed if the requested");
+ System.out.println("information is unavailable or not permitted.");
+ System.out.println("Multiple actions group the results.");
+ System.out.println();
+ System.out.println("See the manual (texdoc texosquery) for further details.");
+
+ System.out.println();
+ System.out.println("Options:");
+ System.out.println();
+
+ System.out.println("-h or --help or -help");
+ System.out.println("\tDisplay this help message and exit.");
+ System.out.println();
+
+ System.out.println("-v or --version or -version");
+ System.out.println("\tDisplay version information and exit.");
+ System.out.println();
+
+ System.out.println("--nodebug");
+ System.out.println("\tNo debugging messages (default)");
+ System.out.println();
+
+ System.out.println("--debug [<n>] or -debug [<n>]");
+ System.out.println("\tDisplay debugging messages on STDOUT.");
+ System.out.println("\t<n> should be an integer:");
+ System.out.println("\t0: no debugging (same as --nodebug)");
+ System.out.println("\t1: basic debugging messages");
+ System.out.println("\t2: additionally display stack trace.");
+ System.out.println(String.format("\tIf omitted %d is assumed", DEFAULT_DEBUG_LEVEL));
+ System.out.println();
+
+ System.out.println("--compatible <n> or -compat <n>");
+ System.out.println("\tCompatibility setting.");
+ System.out.println("\t<n> should be \"latest\" (default) or an integer:");
+
+ for (int i = 0; i < DEFAULT_COMPATIBLE; i++)
+ {
+ System.out.println(String.format("\t%d: version 1.%d", i, i));
+ }
+
+ System.out.println();
+ System.out.println("General actions:");
+ System.out.println();
+
+ for (QueryAction action : AVAILABLE_ACTIONS)
+ {
+ if (action.getType() == QueryActionType.GENERAL_ACTION)
+ {
+ System.out.println(action.help());
+ }
+ }
+
+ System.out.println();
+ System.out.println("Locale actions:");
+ System.out.println();
+
+ for (QueryAction action : AVAILABLE_ACTIONS)
+ {
+ if (action.getType() == QueryActionType.LOCALE_ACTION)
+ {
+ System.out.println(action.help());
+ }
+ }
+
+ System.out.println();
+ System.out.println("File actions:");
+ System.out.println();
+ System.out.println("Paths should use / for the directory divider.");
+ System.out.println("TeX's openin_any setting is checked before attempting");
+ System.out.println("to access file information.");
+ System.out.println();
+
+ for (QueryAction action : AVAILABLE_ACTIONS)
+ {
+ if (action.getType() == QueryActionType.FILE_ACTION)
+ {
+ System.out.println(action.help());
+ }
+ }
+ }
+
+ /**
+ * Prints the version.
+ */
+ protected void version()
+ {
+ System.out.println(String.format("%s %s %s", name, VERSION_NUMBER,
+ VERSION_DATE));
+ System.out.println("Copyright 2016 Nicola Talbot");
+ System.out.println("License LPPL 1.3+ (http://ctan.org/license/lppl1.3)");
+ }
+
+ /**
+ * Prints the information with optional grouping.
+ * @param numActions Add grouping if number of actions &gt; 1
+ * @param info Information to print
+ * @since 1.2
+ */
+ protected void print(int numActions, String info)
+ {
+ if (compatible == 0)
+ {
+ // version 1.0 didn't use grouping
+ System.out.println(info);
+ }
+ else
+ {
+ if (numActions > 1)
+ {
+ System.out.println(String.format("{%s}", info));
+ }
+ else
+ {
+ System.out.println(info);
+ }
+ }
+ }
+
+ /**
+ * Find the action corresponding to the name (the command line
+ * switch). Once the action has been found, a copy must be
+ * returned since the same action may be used multiple times with
+ * different arguments.
+ * @param action The command line switch (either the short or long
+ * form)
+ * @return a copy of the predefined action or null if not found
+ * @since 1.2
+ */
+ private QueryAction getAction(String action)
+ {
+ for (int i = 0; i < AVAILABLE_ACTIONS.length; i++)
+ {
+ if (AVAILABLE_ACTIONS[i].isAction(action))
+ {
+ return AVAILABLE_ACTIONS[i].copy();
+ }
+ }
+
+ return null;
+ }
+
+ public static int parseArgVal(String[] args, int i, Object[] argVal)
+ {
+ String[] sp;
+
+ if (args[i].startsWith("--"))
+ {
+ sp = args[i].split("=", 2);
+ }
+ else
+ {
+ sp = new String[]{args[i]};
+ }
+
+ argVal[0] = sp[0];
+
+ if (sp.length == 2)
+ {
+ argVal[1] = sp[1];
+ return i;
+ }
+
+ if (i == args.length-1 || args[i+1].startsWith("-"))
+ {
+ argVal[1] = null;
+ return i;
+ }
+
+ argVal[1] = args[++i];
+
+ return i;
+ }
+
+ public static int parseArgInt(String[] args, int i, Object[] argVal)
+ {
+ i = parseArgVal(args, i, argVal);
+
+ if (argVal[1] != null)
+ {
+ try
+ {
+ argVal[1] = new Integer((String)argVal[1]);
+ }
+ catch (NumberFormatException e)
+ {
+ throw new IllegalArgumentException(String.format(
+ "Invalid '%s' value: %s", argVal[0], argVal[1]), e);
+ }
+ }
+
+ return i;
+ }
+
+ public static boolean isArg(String arg, String shortArg, String longArg)
+ {
+ return arg.equals("-"+shortArg) || arg.equals("--"+longArg)
+ || arg.startsWith("--"+longArg+"=");
+ }
+
+
+ public static boolean isArg(String arg, String longArg)
+ {
+ return arg.equals("--"+longArg) || arg.startsWith("--"+longArg+"=");
+ }
+
+ /**
+ * Process command line arguments. Options must come before
+ * actions. (The copied QueryAction objects retain the settings
+ * from the time of their creation.)
+ * @param args Command line arguments.
+ * @since 1.2
+ */
+ public void processArgs(String[] args)
+ {
+ Vector<QueryAction> actions = new Vector<QueryAction>();
+ Object[] argVal = new Object[2];
+
+ for (int i = 0; i < args.length; i++)
+ {
+ QueryAction action = getAction(args[i]);
+
+ if (action != null)
+ {
+ try
+ {
+ i = action.parseArgs(args, i)-1;
+ actions.add(action);
+ }
+ catch (IllegalArgumentException e)
+ {
+ debug(e.getMessage(), e);
+ throw e;
+ }
+ catch (Throwable e)
+ {
+ debug(e.getMessage(), e);
+ throw new IllegalArgumentException(e.getMessage(), e);
+ }
+ }
+ else if (args[i].equals("-h") || args[i].equals("--help")
+ || args[i].equals("-help"))
+ {
+ syntax();
+ System.exit(0);
+ }
+ else if (args[i].equals("-v") || args[i].equals("--version")
+ || args[i].equals("-version"))
+ {
+ version();
+ System.exit(0);
+ }
+ else if (args[i].equals("--nodebug"))
+ {
+ if (actions.size() > 0)
+ {
+ throw new IllegalArgumentException(String.format(
+ "Options must come before actions. Found option: %s", args[i]));
+ }
+
+ debugLevel = 0;
+ }
+ else if (isArg(args[i], "debug", "debug"))
+ {
+ if (actions.size() > 0)
+ {
+ throw new IllegalArgumentException(String.format(
+ "Options must come before actions. Found option: %s", args[i]));
+ }
+
+ i = parseArgInt(args, i, argVal);
+
+ if (argVal[1] == null)
+ {
+ debugLevel = DEFAULT_DEBUG_LEVEL;
+ }
+ else
+ {
+ debugLevel = ((Integer)argVal[1]).intValue();
+
+ if (debugLevel < 0)
+ {
+ throw new IllegalArgumentException(String.format(
+ "Invalid debug level: %s", args[i]));
+ }
+ }
+ }
+ else if (args[i].equals("--compatible") || args[i].equals("-compat"))
+ {
+ if (actions.size() > 0)
+ {
+ throw new IllegalArgumentException(String.format(
+ "Options must come before actions. Found option: %s", args[i]));
+ }
+
+ i = parseArgVal(args, i, argVal);
+
+ if (argVal[1] == null)
+ {
+ throw new IllegalArgumentException(String.format(
+ "<level> expected after: %s", args[i]));
+ }
+
+ if (argVal[1].equals("latest"))
+ {
+ compatible = DEFAULT_COMPATIBLE;
+ }
+ else
+ {
+ try
+ {
+ compatible = Integer.parseInt((String)argVal[1]);
+ }
+ catch (NumberFormatException e)
+ {
+ throw new IllegalArgumentException(String.format(
+ "Invalid %s argument (\"latest\" or %d to %d required): %s",
+ argVal[0], 0, DEFAULT_COMPATIBLE, argVal[1]), e);
+ }
+ }
+ }
+ else
+ {
+ throw new IllegalArgumentException(String.format(
+ "Unknown option: %s%nTry %s --help", args[i], name));
+ }
+ }
+
+ int numActions = actions.size();
+
+ if (numActions == 0)
+ {
+ throw new IllegalArgumentException(String.format(
+ "One or more actions required.%nTry %s --help", name));
+ }
+
+ for (QueryAction action : actions)
+ {
+ try
+ {
+ print(numActions, action.doAction(compatible));
+ }
+ catch (Throwable e)
+ {
+ // Any errors should've been picked up by the action,
+ // so this is most likely a runtime error that needs
+ // to be reported.
+
+ System.err.println("Fatal error: "+e.getMessage());
+
+ if (debugLevel < DEBUG_STACK_TRACE_LEVEL)
+ {
+ System.err.println(String.format(
+ "Use --debug %d to obtain stack trace",
+ DEBUG_STACK_TRACE_LEVEL));
+ }
+
+ debug(String.format("Action failed: %s", action.getInvocation()),
+ e);
+ System.exit(1);
+ }
+ }
+ }
+
+ private final QueryAction[] AVAILABLE_ACTIONS = new QueryAction[]
+ {
+ new QueryAction("cwd", "c", QueryActionType.FILE_ACTION,
+ "Display current working directory")
+ {
+ public String action()
+ {
+ return getCwd();
+ }
+ },
+ new QueryAction("userhome", "m", QueryActionType.FILE_ACTION,
+ "Display user's home directory")
+ {
+ public String action()
+ {
+ return getUserHome();
+ }
+ },
+ new QueryAction("tmpdir", "t", QueryActionType.FILE_ACTION,
+ "Display temporary directory")
+ {
+ public String action()
+ {
+ return getTmpDir();
+ }
+ },
+ new QueryAction("osname", "o", QueryActionType.GENERAL_ACTION,
+ "Display OS name")
+ {
+ public String action()
+ {
+ return getOSname();
+ }
+ },
+ new QueryAction("osversion", "r", QueryActionType.GENERAL_ACTION,
+ "Display OS version")
+ {
+ public String action()
+ {
+ return getOSversion();
+ }
+ },
+ new QueryAction("osarch", "a", QueryActionType.GENERAL_ACTION,
+ "Display OS architecture")
+ {
+ public String action()
+ {
+ return getOSarch();
+ }
+ },
+ new QueryAction("pdfnow", "n", QueryActionType.GENERAL_ACTION,
+ "Display current date-time in PDF format")
+ {
+ public String action()
+ {
+ return pdfnow();
+ }
+ },
+ new QueryAction("locale", "L", QueryActionType.LOCALE_ACTION,
+ "Display POSIX locale information")
+ {
+ public String action()
+ {
+ return getLocale(Locale.getDefault());
+ }
+ },
+ new QueryAction("locale-lcs", "l", QueryActionType.LOCALE_ACTION,
+ "Display POSIX style locale information with lower case codeset")
+ {
+ public String action()
+ {
+ return getLocale(Locale.getDefault(), true);
+ }
+ },
+ new QueryAction("codeset-lcs", "C", QueryActionType.GENERAL_ACTION,
+ "Lower case codeset with hyphens stripped", 2)
+ {
+ public String action()
+ {
+ return escapeFileName(getCodeSet(true));
+ }
+ },
+ new QueryAction("bcp47", "b", QueryActionType.LOCALE_ACTION,
+ "Display locale as BCP47 tag", 2)
+ {
+ public String action()
+ {
+ return escapeFileName(getLanguageTag(null));
+ }
+ },
+ new QueryAction("numeric", "N", 1, 0, "[locale]",
+ QueryActionType.LOCALE_ACTION,
+ "Display locale numeric information", 2)
+ {
+ public String action()
+ {
+ return getNumericalInfo(getOptionalArgument(0));
+ }
+ },
+ new QueryAction("locale-data", "D", 1, 0, "[locale]",
+ QueryActionType.LOCALE_ACTION,
+ "Display all available locale information", 2)
+ {
+ public String action()
+ {
+ return getLocaleData(getOptionalArgument(0));
+ }
+ },
+ new QueryAction("date-time", "M",
+ QueryActionType.GENERAL_ACTION,
+ "Display all the current date-time data", 2)
+ {
+ public String action()
+ {
+ return getDateTimeData();
+ }
+ },
+ new QueryAction("time-zones", "Z", 1, 0, "[locale]",
+ QueryActionType.LOCALE_ACTION,
+ "Display all available time zone information", 2)
+ {
+ public String action()
+ {
+ return getTimeZones(getOptionalArgument(0));
+ }
+ },
+ new QueryAction("pdfdate", "d", 0, 1, "<file>",
+ QueryActionType.FILE_ACTION,
+ "Display date stamp of <file> in PDF format")
+ {
+ public String action()
+ {
+ return pdfDate(fileFromTeXPath(getRequiredArgument(0)));
+ }
+ },
+ new QueryAction("filesize", "s", 0, 1, "<file>",
+ QueryActionType.FILE_ACTION,
+ "Display size of <file> in bytes")
+ {
+ public String action()
+ {
+ return getFileLength(fileFromTeXPath(getRequiredArgument(0)));
+ }
+ },
+ new QueryAction("list", "i", 1, 2, "<sep> <dir> [<sort>]",
+ QueryActionType.FILE_ACTION,
+ String.format("Display list of all files in <dir> separated by <sep>. If <sort> is omitted, the default order is used otherwise <sort> may be one of the following: %s",
+ FileSortType.getFileSortOptions()))
+ {
+ public String action()
+ {
+ return getFileList(getRequiredArgument(0),
+ new File(fromTeXPath(getRequiredArgument(1))),
+ FileSortType.getFileSortType(getOptionalArgument(0)),
+ FileListType.FILE_LIST_ANY);
+ }
+ },
+ new QueryAction("filterlist", "f", 1, 3, "<sep> <regex> <dir> [<sort>]",
+ QueryActionType.FILE_ACTION,
+ String.format("Display list of files in <dir> that fully match <regex> separated by <sep>. If <sort> is omitted, the default order is used otherwise <sort> may be one of the following: %s",
+ FileSortType.getFileSortOptions()))
+ {
+ public String action()
+ {
+ return getFilterFileList(
+ getRequiredArgument(0),
+ getRequiredArgument(1),
+ new File(fromTeXPath(getRequiredArgument(2))),
+ FileSortType.getFileSortType(getOptionalArgument(0)),
+ FileListType.FILE_LIST_ANY);
+ }
+ },
+ new QueryAction("list-dir", "id", 1, 2, "<sep> <dir> [<sort>]",
+ QueryActionType.FILE_ACTION,
+ String.format("Display list of all sub-directories in <dir> separated by <sep>. If <sort> is omitted, the default order is used otherwise <sort> may be one of the following: %s",
+ FileSortType.getFileSortOptions()))
+ {
+ public String action()
+ {
+ return getFileList(getRequiredArgument(0),
+ new File(fromTeXPath(getRequiredArgument(1))),
+ FileSortType.getFileSortType(getOptionalArgument(0)),
+ FileListType.FILE_LIST_DIRECTORIES_ONLY);
+ }
+ },
+ new QueryAction("filterlist-dir", "fd", 1, 3, "<sep> <regex> <dir> [<sort>]",
+ QueryActionType.FILE_ACTION,
+ String.format("Display list of sub-directories in <dir> that fully match <regex> separated by <sep>. If <sort> is omitted, the default order is used otherwise <sort> may be one of the following: %s",
+ FileSortType.getFileSortOptions()))
+ {
+ public String action()
+ {
+ return getFilterFileList(
+ getRequiredArgument(0),
+ getRequiredArgument(1),
+ new File(fromTeXPath(getRequiredArgument(2))),
+ FileSortType.getFileSortType(getOptionalArgument(0)),
+ FileListType.FILE_LIST_DIRECTORIES_ONLY);
+ }
+ },
+ new QueryAction("list-regular", "ir", 1, 2, "<sep> <dir> [<sort>]",
+ QueryActionType.FILE_ACTION,
+ String.format("Display list of all regular files in <dir> separated by <sep>. If <sort> is omitted, the default order is used otherwise <sort> may be one of the following: %s",
+ FileSortType.getFileSortOptions()))
+ {
+ public String action()
+ {
+ return getFileList(getRequiredArgument(0),
+ new File(fromTeXPath(getRequiredArgument(1))),
+ FileSortType.getFileSortType(getOptionalArgument(0)),
+ FileListType.FILE_LIST_REGULAR_FILES_ONLY);
+ }
+ },
+ new QueryAction("filterlist-regular", "fr", 1, 3, "<sep> <regex> <dir> [<sort>]",
+ QueryActionType.FILE_ACTION,
+ String.format("Display list of regular files in <dir> that fully match <regex> separated by <sep>. If <sort> is omitted, the default order is used otherwise <sort> may be one of the following: %s",
+ FileSortType.getFileSortOptions()))
+ {
+ public String action()
+ {
+ return getFilterFileList(
+ getRequiredArgument(0),
+ getRequiredArgument(1),
+ new File(fromTeXPath(getRequiredArgument(2))),
+ FileSortType.getFileSortType(getOptionalArgument(0)),
+ FileListType.FILE_LIST_REGULAR_FILES_ONLY);
+ }
+ },
+ new QueryAction("walk", "w", 1, 3, "<sep> <regex> <dir> [<sort>]",
+ QueryActionType.FILE_ACTION,
+ String.format("Display list of regular non-hidden files in <dir> (descending sub-directories) that fully match <regex> separated by <sep>. The starting directory <dir> may not be outside the current working directory. This action is not available for texosquery-jre5. If <sort> is omitted, the default order is used otherwise <sort> may be one of the following: %s",
+ FileSortType.getFileSortOptions()), 2)
+ {
+ public String action()
+ {
+ return walk(
+ getRequiredArgument(0),
+ getRequiredArgument(1),
+ new File(fromTeXPath(getRequiredArgument(2))),
+ FileSortType.getFileSortType(getOptionalArgument(0)));
+ }
+ },
+ new QueryAction("uri", "u", 0, 1, "<file>",
+ QueryActionType.FILE_ACTION, "Display the URI of <file>")
+ {
+ public String action()
+ {
+ return fileURI(fileFromTeXPath(getRequiredArgument(0)));
+ }
+ },
+ new QueryAction("path", "p", 0, 1, "<file>",
+ QueryActionType.FILE_ACTION, "Display the canonical path of <file>")
+ {
+ public String action()
+ {
+ return filePath(fileFromTeXPath(getRequiredArgument(0)));
+ }
+ },
+ new QueryAction("dirname", "e", 0, 1, "<file>",
+ QueryActionType.FILE_ACTION,
+ "Display the canonical path of the parent of <file>")
+ {
+ public String action()
+ {
+ return parentPath(fileFromTeXPath(getRequiredArgument(0)));
+ }
+ }
+ };
+
+ /**
+ * Application name.
+ */
+ private String name;
+
+ public static final int DEFAULT_COMPATIBLE=2;
+
+ private static final String VERSION_NUMBER = "1.2";
+ private static final String VERSION_DATE = "2017-03-23";
+ private static final char BACKSLASH = '\\';
+ private static final long ZERO = 0L;
+
+ /**
+ * Initialise current date-time for consistency.
+ */
+
+ private Date now = new Date();
+
+ /**
+ * openin_any settings
+ */
+ private static final char OPENIN_UNSET=0; // unset
+ private static final char OPENIN_A='a'; // any
+ private static final char OPENIN_R='r'; // restricted
+ private static final char OPENIN_P='p'; // paranoid
+
+ private char openin = OPENIN_UNSET;
+
+ private File texmfoutput = null;
+
+ /**
+ * Debug level. (0 = no debugging, 1 or more print error messages to
+ * STDERR, 2 or more include stack trace, 3 or more include
+ * informational messages.)
+ */
+ private int debugLevel = 0;
+
+ public static final int DEFAULT_DEBUG_LEVEL=3;
+ public static final int DEBUG_ERROR_LEVEL=1;
+ public static final int DEBUG_STACK_TRACE_LEVEL=2;
+ public static final int DEBUG_INFO_LEVEL=3;
+
+ /**
+ * Compatibility mode. Version 1.2 replaces escapeHash with
+ * escapeSpChars, which switches to using \\fhsh etc. Provide a
+ * mode to restore the previous behaviour.
+ */
+ private int compatible = DEFAULT_COMPATIBLE;
+
+ // TeX can only go up to 2147483647, so set the maximum number
+ // of digits provided for the number formatter.
+
+ private static final int MAX_DIGIT_FORMAT=10;
+
+ // Dollar symbol
+ private static final char DOLLAR_CHAR=0x0024;
+
+ // Cent symbol
+ private static final char CENT_CHAR=0x00A2;
+
+ // Pound symbol
+ private static final char POUND_CHAR=0x00A3;
+
+ // Pound symbol as a string
+ private static final String POUND_STRING=""+POUND_CHAR;
+
+ // Currency symbol
+ private static final char CURRENCY_CHAR=0x00A4;
+
+ // Yen symbol
+ private static final char YEN_CHAR=0x00A5;
+
+ // ECU symbol
+ private static final char ECU_CHAR=0x20A0;
+
+ // Colon currency symbol
+ private static final char COLON_CURRENCY_CHAR=0x20A1;
+
+ // Cruzeiro symbol
+ private static final char CRUZEIRO_CHAR=0x20A2;
+
+ // Franc symbol
+ private static final char FRANC_CHAR=0x20A3;
+
+ // Lira symbol
+ private static final char LIRA_CHAR=0x20A4;
+
+ // Mill currency symbol
+ private static final char MILL_CURRENCY_CHAR=0x20A5;
+
+ // Naira symbol
+ private static final char NAIRA_CHAR=0x20A6;
+
+ // Peseta symbol
+ private static final char PESETA_CHAR=0x20A7;
+
+ // Legacy rupee symbol
+ private static final char LEGACY_RUPEE_CHAR=0x20A8;
+
+ // Won symbol
+ private static final char WON_CHAR=0x20A9;
+
+ // New sheqel symbol
+ private static final char NEW_SHEQEL_CHAR=0x20AA;
+
+ // Dong symbol
+ private static final char DONG_CHAR=0x20AB;
+
+ // Euro symbol
+ private static final char EURO_CHAR=0x20AC;
+
+ // Kip symbol
+ private static final char KIP_CHAR=0x20AD;
+
+ // Tugrik symbol
+ private static final char TUGRIK_CHAR=0x20AE;
+
+ // Drachma symbol
+ private static final char DRACHMA_CHAR=0x20AF;
+
+ // German penny symbol
+ private static final char GERMAN_PENNY_CHAR=0x20B0;
+
+ // Peso symbol
+ private static final char PESO_CHAR=0x20B1;
+
+ // Guarani symbol
+ private static final char GUARANI_CHAR=0x20B2;
+
+ // Austral symbol
+ private static final char AUSTRAL_CHAR=0x20B3;
+
+ // Hryvnia symbol
+ private static final char HRYVNIA_CHAR=0x20B4;
+
+ // Cedi symbol
+ private static final char CEDI_CHAR=0x20B5;
+
+ // Livre tournois symbol
+ private static final char LIVRE_TOURNOIS_CHAR=0x20B6;
+
+ // Spesmilo symbol
+ private static final char SPESMILO_CHAR=0x20B7;
+
+ // Tenge symbol
+ private static final char TENGE_CHAR=0x20B8;
+
+ // Official rupee symbol
+ private static final char RUPEE_CHAR=0x20B9;
+
+ // Turkish lira symbol
+ private static final char TURKISH_LIRA_CHAR=0x20BA;
+
+ // Nordic mark symbol
+ private static final char NORDIC_MARK_CHAR=0x20BB;
+
+ // Manat symbol
+ private static final char MANAT_CHAR=0x20BC;
+
+ // Ruble symbol
+ private static final char RUBLE_CHAR=0x20BD;
+
+ // Per mille symbol
+ private static final char PERMILLE_CHAR=0x2030;
+}
diff --git a/Master/texmf-dist/source/support/texosquery/java/TeXOSQueryJRE5.java b/Master/texmf-dist/source/support/texosquery/java/TeXOSQueryJRE5.java
new file mode 100644
index 00000000000..15c6bd6c50a
--- /dev/null
+++ b/Master/texmf-dist/source/support/texosquery/java/TeXOSQueryJRE5.java
@@ -0,0 +1,54 @@
+/*
+ Copyright (C) 2016 Nicola L.C. Talbot
+ www.dickimaw-books.com
+
+ This work may be distributed and/or modified under the
+ conditions of the LaTeX Project Public License, either version 1.3
+ of this license or (at your option) any later version.
+ The latest version of this license is in
+ http://www.latex-project.org/lppl.txt
+ and version 1.3 or later is part of all distributions of LaTeX
+ version 2005/12/01 or later.
+*/
+package com.dickimawbooks.texosquery;
+
+import java.io.BufferedReader;
+import java.util.Locale;
+import java.util.Calendar;
+import java.text.DecimalFormatSymbols;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+/**
+ * Main class. Supports Java 5 onwards.
+ * @author Nicola Talbot
+ * @version 1.2
+ * @since 1.2
+ */
+public class TeXOSQueryJRE5 extends TeXOSQuery
+{
+ public TeXOSQueryJRE5()
+ {
+ super("texosquery-jre5");
+ }
+
+ /**
+ * Main method.
+ * @param args Command line arguments.
+ */
+ public static void main(String[] args)
+ {
+ try
+ {
+ (new TeXOSQueryJRE5()).processArgs(args);
+ }
+ catch (IllegalArgumentException e)
+ {
+ System.err.println(e.getMessage());
+ System.exit(1);
+ }
+ }
+}
diff --git a/Master/texmf-dist/source/support/texosquery/java/TeXOSQueryJRE7.java b/Master/texmf-dist/source/support/texosquery/java/TeXOSQueryJRE7.java
new file mode 100644
index 00000000000..0abb09d0aa6
--- /dev/null
+++ b/Master/texmf-dist/source/support/texosquery/java/TeXOSQueryJRE7.java
@@ -0,0 +1,197 @@
+/*
+ Copyright (C) 2016 Nicola L.C. Talbot
+ www.dickimaw-books.com
+
+ This work may be distributed and/or modified under the
+ conditions of the LaTeX Project Public License, either version 1.3
+ of this license or (at your option) any later version.
+ The latest version of this license is in
+ http://www.latex-project.org/lppl.txt
+ and version 1.3 or later is part of all distributions of LaTeX
+ version 2005/12/01 or later.
+*/
+package com.dickimawbooks.texosquery;
+
+import java.util.Locale;
+import java.util.Locale.Builder;
+import java.util.Calendar;
+import java.io.Serializable;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Main class. Supports Java 7 onwards.
+ * @author Nicola Talbot
+ * @version 1.2
+ * @since 1.2
+ */
+public class TeXOSQueryJRE7 extends TeXOSQuery
+{
+ public TeXOSQueryJRE7()
+ {
+ super("texosquery");
+ }
+
+ /**
+ * Converts the given directory to a canonical path
+ * and checks to make sure it has a parent directory.
+ * @param dir the directory to check
+ * @return the canonical path
+ * @throws IOException if directory can't be converted to a
+ * canonical path or if the canonical path doesn't have a parent
+ * directory or is outside the current working directory
+ */
+ @Override
+ protected File checkDirectoryListing(File dir) throws IOException
+ {
+ dir = dir.getCanonicalFile();
+
+ if (dir.getParentFile() == null)
+ {
+ throw new IOException(String.format(
+ "Listing on root directory not permitted: %s", dir));
+ }
+
+ File cwd = new File(getSystemProperty("user.dir", "."));
+
+ if (!cwd.equals(dir) && !isFileInTree(dir, cwd))
+ {
+ throw new IOException(String.format(
+ "Listing outside cwd path not permitted: %s", dir));
+ }
+
+ return dir;
+ }
+
+ /**
+ * Recursive file listing. This method must have the CWD or a
+ * descendent as the starting directory. It will return list of
+ * files relative to the starting directory where the basename
+ * matches the supplied regular expression. Hidden files/directories
+ * and symbolic links are skipped regardless of the openin_any setting.
+ * Files without read access are also omitted from the list.
+ *
+ * @param separator separator to use in returned list
+ * @param regex regular expression used to match file basenames
+ * @param directory starting directory (must be cwd or a
+ * descendent of cwd)
+ * @return list of relative paths
+ */
+ @Override
+ public String walk(String separator,
+ String regex, File directory,
+ FileSortType sortType)
+ {
+ try
+ {
+ return FileWalkVisitor.walk(this, separator,
+ regex, directory, sortType);
+ }
+ catch (Exception e)
+ {
+ debug(String.format("Can't walk directory: %s",
+ directory.toString()), e);
+ }
+
+ return "";
+ }
+
+ /**
+ * Gets the script for the given locale.
+ * @param locale The locale
+ * @return The language script associated with the given locale or null if not available
+ */
+ @Override
+ public String getScript(Locale locale)
+ {
+ return locale.getScript();
+ }
+
+ /**
+ * Gets the locale from the given language tag.
+ * @param languageTag The language tag
+ * @return The locale that closest matches the language tag
+ */
+ @Override
+ public Locale getLocale(String languageTag)
+ {
+ Locale locale = Locale.forLanguageTag(languageTag);
+
+ // Locale.forLanguageTag() doesn't recognise
+ // numeric regions. So test for a numeric region.
+
+ String region = locale.getCountry();
+
+ try
+ {
+ region = getRegionAlpha2Code(Integer.parseInt(region));
+ }
+ catch (NumberFormatException e)
+ {
+ // region isn't numeric, so we don't need to do anything
+ // else
+ return locale;
+ }
+
+ Locale.Builder builder = new Locale.Builder();
+ builder.setLocale(locale);
+ builder.setRegion(region);
+
+ return builder.build();
+ }
+
+ /**
+ * Gets the language tag for the given locale.
+ * @param locale The locale or null for the default locale
+ * @return The language tag
+ */
+ @Override
+ public String getLanguageTag(Locale locale)
+ {
+ if (locale == null)
+ {
+ locale = Locale.getDefault();
+ }
+
+ return locale.toLanguageTag();
+ }
+
+ /**
+ * Gets the week year for the given calendar.
+ * @return The week year
+ */
+ @Override
+ public int getWeekYear(Calendar cal)
+ {
+ try
+ {
+ return cal.isWeekDateSupported() ?
+ cal.getWeekYear() : cal.get(Calendar.YEAR);
+ }
+ catch (UnsupportedOperationException e)
+ {
+ // shouldn't happen with the above conditional, but just in
+ // case...
+
+ debug(e.getMessage(), e);
+ return cal.get(Calendar.YEAR);
+ }
+ }
+
+ /**
+ * Main method.
+ * @param args Command line arguments.
+ */
+ public static void main(String[] args)
+ {
+ try
+ {
+ (new TeXOSQueryJRE7()).processArgs(args);
+ }
+ catch (IllegalArgumentException e)
+ {
+ System.err.println(e.getMessage());
+ System.exit(1);
+ }
+ }
+}
diff --git a/Master/texmf-dist/source/support/texosquery/java/TeXOSQueryJRE8.java b/Master/texmf-dist/source/support/texosquery/java/TeXOSQueryJRE8.java
new file mode 100644
index 00000000000..ab65d64bbf5
--- /dev/null
+++ b/Master/texmf-dist/source/support/texosquery/java/TeXOSQueryJRE8.java
@@ -0,0 +1,377 @@
+/*
+ Copyright (C) 2016 Nicola L.C. Talbot
+ www.dickimaw-books.com
+
+ This work may be distributed and/or modified under the
+ conditions of the LaTeX Project Public License, either version 1.3
+ of this license or (at your option) any later version.
+ The latest version of this license is in
+ http://www.latex-project.org/lppl.txt
+ and version 1.3 or later is part of all distributions of LaTeX
+ version 2005/12/01 or later.
+*/
+package com.dickimawbooks.texosquery;
+
+import java.util.Locale;
+import java.util.Locale.Builder;
+import java.util.Calendar;
+import java.util.Map;
+import java.util.Arrays;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Main class. Supports Java 8 onwards.
+ * @author Nicola Talbot
+ * @version 1.2
+ * @since 1.2
+ */
+public class TeXOSQueryJRE8 extends TeXOSQuery
+{
+ public TeXOSQueryJRE8()
+ {
+ super("texosquery-jre8");
+ }
+
+ /**
+ * Converts the given directory to a canonical path
+ * and checks to make sure it has a parent directory.
+ * @param dir the directory to check
+ * @return the canonical path
+ * @throws IOException if directory can't be converted to a
+ * canonical path or if the canonical path doesn't have a parent
+ * directory or is outside the current working directory
+ */
+ @Override
+ protected File checkDirectoryListing(File dir) throws IOException
+ {
+ dir = dir.getCanonicalFile();
+
+ if (dir.getParentFile() == null)
+ {
+ throw new IOException(String.format(
+ "Listing on root directory not permitted: %s", dir));
+ }
+
+ File cwd = new File(getSystemProperty("user.dir", "."));
+
+ if (!cwd.equals(dir) && !isFileInTree(dir, cwd))
+ {
+ throw new IOException(String.format(
+ "Listing outside cwd path not permitted: %s", dir));
+ }
+
+ return dir;
+ }
+
+ /**
+ * Recursive file listing. This method must have the CWD or a
+ * descendent as the starting directory. It will return list of
+ * files relative to the starting directory where the basename
+ * matches the supplied regular expression. Hidden files/directories
+ * and symbolic links are skipped regardless of the openin_any setting.
+ * Files without read access are also omitted from the list.
+ *
+ * @param separator separator to use in returned list
+ * @param regex regular expression used to match file basenames
+ * @param directory starting directory (must be cwd or a
+ * descendent of cwd)
+ * @return list of relative paths
+ */
+ @Override
+ public String walk(String separator,
+ String regex, File directory,
+ FileSortType sortType)
+ {
+ try
+ {
+ return FileWalkVisitor.walk(this, separator,
+ regex, directory, sortType);
+ }
+ catch (Exception e)
+ {
+ debug(String.format("Can't walk directory: %s",
+ directory.toString()), e);
+ }
+
+ return "";
+ }
+
+
+ /**
+ * Gets the script for the given locale.
+ * @param locale The locale
+ * @return The language script associated with the given locale or null if not available
+ */
+ @Override
+ public String getScript(Locale locale)
+ {
+ return locale.getScript();
+ }
+
+ /**
+ * Gets the locale from the given language tag.
+ * @param languageTag The language tag
+ * @return The locale that closest matches the language tag
+ */
+ @Override
+ public Locale getLocale(String languageTag)
+ {
+ Locale locale = Locale.forLanguageTag(languageTag);
+
+ // Locale.forLanguageTag() doesn't recognise
+ // numeric regions. So test for a numeric region.
+
+ String region = locale.getCountry();
+
+ try
+ {
+ region = getRegionAlpha2Code(Integer.parseInt(region));
+ }
+ catch (NumberFormatException e)
+ {
+ // region isn't numeric, so we don't need to do anything
+ // else
+ return locale;
+ }
+
+ Locale.Builder builder = new Locale.Builder();
+ builder.setLocale(locale);
+ builder.setRegion(region);
+
+ return builder.build();
+ }
+
+ /**
+ * Gets the language tag for the given locale.
+ * @param locale The locale or null for the default locale
+ * @return The language tag
+ */
+ @Override
+ public String getLanguageTag(Locale locale)
+ {
+ if (locale == null)
+ {
+ locale = Locale.getDefault();
+ }
+
+ return locale.toLanguageTag();
+ }
+
+ /**
+ * Gets the week year for the given calendar.
+ * @return The week year
+ */
+ @Override
+ public int getWeekYear(Calendar cal)
+ {
+ try
+ {
+ return cal.isWeekDateSupported() ?
+ cal.getWeekYear() : cal.get(Calendar.YEAR);
+ }
+ catch (UnsupportedOperationException e)
+ {
+ // shouldn't happen with the above conditional, but just in
+ // case...
+
+ debug(e.getMessage(), e);
+ return cal.get(Calendar.YEAR);
+ }
+ }
+
+ /** Gets the standalone month names for the locale data.
+ * These are only available for Java 8, so just return the
+ * month names used in the date format instead.
+ * @param cal The calendar
+ * @param locale The locale
+ * @return month names
+ */
+ public String getStandaloneMonths(Calendar cal, Locale locale)
+ {
+ Map<String,Integer> map = cal.getDisplayNames(Calendar.MONTH,
+ Calendar.LONG_STANDALONE, locale);
+
+ // Is the map order? Not sure so save in an array
+
+ String[] names = new String[12];
+
+ for (Map.Entry<String,Integer> entry : map.entrySet())
+ {
+ int idx = entry.getValue().intValue();
+
+ if (idx < 12)
+ {// Java has a 13th month that we're ignoring
+ names[idx] = entry.getKey();
+ }
+ }
+
+ StringBuilder builder = new StringBuilder();
+
+ for (int i = 0; i < names.length; i++)
+ {
+ builder.append(String.format("{%s}", escapeText(names[i])));
+ }
+
+ return builder.toString();
+ }
+
+ /** Gets the standalone short month names for the locale data.
+ * @param cal The calendar
+ * @param locale The locale
+ * @return month names
+ */
+ public String getStandaloneShortMonths(Calendar cal, Locale locale)
+ {
+ Map<String,Integer> map = cal.getDisplayNames(Calendar.MONTH,
+ Calendar.SHORT_STANDALONE, locale);
+
+ // Is the map order? Not sure so save in an array
+
+ String[] names = new String[12];
+
+ for (Map.Entry<String,Integer> entry : map.entrySet())
+ {
+ int idx = entry.getValue().intValue();
+
+ if (idx < names.length)
+ {
+ names[idx] = entry.getKey();
+ }
+ }
+
+ StringBuilder builder = new StringBuilder();
+
+ for (int i = 0; i < names.length; i++)
+ {
+ builder.append(String.format("{%s}", escapeText(names[i])));
+ }
+
+ return builder.toString();
+ }
+
+
+ /** Gets the standalone week day names for the locale data.
+ * @param cal The calendar
+ * @param locale The locale
+ * @return week day names
+ */
+ public String getStandaloneWeekdays(Calendar cal, Locale locale)
+ {
+ Map<String,Integer> map = cal.getDisplayNames(Calendar.DAY_OF_WEEK,
+ Calendar.LONG_STANDALONE, locale);
+
+ String[] names = new String[7];
+
+ for (Map.Entry<String,Integer> entry : map.entrySet())
+ {
+ switch (entry.getValue().intValue())
+ {
+ case Calendar.MONDAY:
+ names[0] = entry.getKey();
+ break;
+ case Calendar.TUESDAY:
+ names[1] = entry.getKey();
+ break;
+ case Calendar.WEDNESDAY:
+ names[2] = entry.getKey();
+ break;
+ case Calendar.THURSDAY:
+ names[3] = entry.getKey();
+ break;
+ case Calendar.FRIDAY:
+ names[4] = entry.getKey();
+ break;
+ case Calendar.SATURDAY:
+ names[5] = entry.getKey();
+ break;
+ case Calendar.SUNDAY:
+ names[6] = entry.getKey();
+ break;
+ }
+ }
+
+ StringBuilder builder = new StringBuilder();
+
+ for (int i = 0; i < names.length; i++)
+ {
+ builder.append(String.format("{%s}", escapeText(names[i])));
+ }
+
+ return builder.toString();
+ }
+
+ /** Gets the standalone short week day names for the locale data.
+ * @param cal The calendar
+ * @param locale The locale
+ * @return week day names
+ */
+ public String getStandaloneShortWeekdays(Calendar cal, Locale locale)
+ {
+ Map<String,Integer> map = cal.getDisplayNames(Calendar.DAY_OF_WEEK,
+ Calendar.SHORT_STANDALONE, locale);
+
+ String[] names = new String[7];
+
+ for (Map.Entry<String,Integer> entry : map.entrySet())
+ {
+ switch (entry.getValue().intValue())
+ {
+ case Calendar.MONDAY:
+ names[0] = entry.getKey();
+ break;
+ case Calendar.TUESDAY:
+ names[1] = entry.getKey();
+ break;
+ case Calendar.WEDNESDAY:
+ names[2] = entry.getKey();
+ break;
+ case Calendar.THURSDAY:
+ names[3] = entry.getKey();
+ break;
+ case Calendar.FRIDAY:
+ names[4] = entry.getKey();
+ break;
+ case Calendar.SATURDAY:
+ names[5] = entry.getKey();
+ break;
+ case Calendar.SUNDAY:
+ names[6] = entry.getKey();
+ break;
+ }
+ }
+
+ StringBuilder builder = new StringBuilder();
+
+ for (int i = 0; i < names.length; i++)
+ {
+ builder.append(String.format("{%s}", escapeText(names[i])));
+ }
+
+ return builder.toString();
+ }
+
+ @Override
+ public void sortFileList(String[] list, File directory,
+ FileSortType sortType)
+ {
+ Arrays.parallelSort(list, new FileSortComparator(directory, sortType));
+ }
+
+ /**
+ * Main method.
+ * @param args Command line arguments.
+ */
+ public static void main(String[] args)
+ {
+ try
+ {
+ (new TeXOSQueryJRE8()).processArgs(args);
+ }
+ catch (IllegalArgumentException e)
+ {
+ System.err.println(e.getMessage());
+ System.exit(1);
+ }
+ }
+}