Skip to content

Commit

Permalink
Enhance SimpleAPI for use through JNI
Browse files Browse the repository at this point in the history
  • Loading branch information
bertfrees committed Oct 4, 2024
2 parents e360117 + 262183f commit ff701bc
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 59 deletions.
53 changes: 53 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,60 @@
Changes in release v1.14.19
===========================

## Framework

- **NEW** API to list TTS services and their status (available, disabled or error)
- **NEW** Access to Sass variables defined in user agent style sheets via `/stylesheet-parameters` API

## Modules

- **NEW** DOCX (MS Word) to DTBook script: this brings some of the functionality of the Word Save-as-DAISY addin to Pipeline
- **NEW** Option for speaking image alt text
- **NEW** "Style sheet parameters" option supported on more scripts (DTBook to DAISY 3, DTBook to EPUB 3, EPUB 3 Enhancer and EPUB to DAISY)
- **NEW** New syntax for "Style sheet parameters" option
- **FIX** Improved error diagnostics for Google and Azure TTS services
- **FIX** Improved logging of the status of TTS services in general (only show relevant services, shorten error messages)
- **FIX** Handle text in `sidebar` not within `p` in DTBook to ODT
- **FIX** DAISY 3 and DAISY 2.02 MegaVoice multi-level scripts do not try to produce files with forbidden characters in their name anymore
- **FIX** Fix NCC metadata in output of DAISY 3 to DAISY 2.02
- **FIX** Support for `rel="pronunciation"` links in DTBook to DAISY 3
- **FIX** Support for Apple Silicon processors
- Changes to braille production scripts, see release notes of braille modules [v1.14.26](https://github.com/daisy/pipeline-modules/blob/master/braille/NEWS.md#v11426)

## Details

See [all the closed issues of this release](https://github.com/orgs/daisy/projects/5). To view using the [Github CLI](https://cli.github.com/): `gh project --owner daisy item-list 5`

Changes in release v1.14.18
===========================

## Framework

- **NEW** API for analyzing CSS style sheets and exposing Sass variables.

## Modules

- **NEW** Script options to specify user lexicons
- **NEW** Global setting for default user lexicon
- **NEW** Global setting for default speaking rate
- **FIX** Major simplification of configuration of braille scripts, see release notes of braille modules [v1.14.24](https://github.com/daisy/pipeline-modules/blob/master/braille/NEWS.md#v11424) and [v1.14.25](https://github.com/daisy/pipeline-modules/blob/master/braille/NEWS.md#v11425)
- **FIX** Fix bugs in handling of language during voice selection.
- **FIX** Fix bugs in processing of aural CSS.
- **FIX** Improved usability of the DAISY 3 and DAISY 2.02 MegaVoice multi-level scripts.
- **FIX** Improve support for DPUB ARIA
- **FIX** Preserve external hyperlinks in EPUB
- **FIX** Include page source identification metadata in EPUB created from DTBook
- Other bugfixes and optimizations

## Details

See [all the closed issues of this release](https://github.com/orgs/daisy/projects/4). To view using the [Github CLI](https://cli.github.com/): `gh project --owner daisy item-list 4`

Changes in release v1.14.17
===========================

## Framework

- **NEW** API for getting and setting TTS and other properties globally

## Modules
Expand Down
172 changes: 116 additions & 56 deletions src/main/java/SimpleAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;
Expand Down Expand Up @@ -36,9 +39,10 @@

/**
* A simplified Java API consisting of a {@link #startJob()} method that starts a job based on a
* script name and a list of options, and {@link #getNewMessages()} and {@link #getLastJobStatus()}
* methods. It is used to build a simple Java CLI (see the {@link #main()} method). The simplified
* API also makes it easier to bridge with other programming languages using JNI.
* script name and a list of options and returns a {@link CommandLineJob}. This object provices
* convenience methods for monitoring the status and messages. This class is used to build a simple
* Java CLI (see the {@link #main()} method). The simplified API also makes it easier to bridge with
* other programming languages using JNI.
*/
@Component(
name = "SimpleAPI",
Expand Down Expand Up @@ -71,7 +75,8 @@ public void setJobFactory(JobFactory jobFactory) {
this.jobFactory = jobFactory;
}

private void _startJob(String scriptName, Map<String,? extends Iterable<String>> options) throws IllegalArgumentException, FileNotFoundException {
private CommandLineJob _startJob(String scriptName, Map<String,? extends Iterable<String>> options)
throws IllegalArgumentException, FileNotFoundException, URISyntaxException {
ScriptService<?> scriptService = scriptRegistry.getScript(scriptName);
if (scriptService == null)
throw new IllegalArgumentException(scriptName + " script not found");
Expand All @@ -82,24 +87,31 @@ private void _startJob(String scriptName, Map<String,? extends Iterable<String>>
for (String value : e.getValue())
parser.withArgument(e.getKey(), value);
CommandLineJob job = parser.createJob(jobFactory);
MessageAccessor accessor = job.getMonitor().getMessageAccessor();
accessor.listen(
num -> {
consumeMessage(accessor, num);
}
);
job.getMonitor().getStatusUpdates().listen(s -> updateJobStatus(s));
new Thread(job).start();
return job;
}

public static void startJob(String scriptName, Map<String,? extends Iterable<String>> options) throws IllegalArgumentException, FileNotFoundException {
getInstance()._startJob(scriptName, options);
/**
* Start a new job
*
* @param scriptName the name of the script
* @param options the command line arguments, providing the inputs and option values for the
* job, and file locations where results must be stored.
* @return The job, wrapped in a {@link CommandLineJob} object for easy monitoring.
*/
public static CommandLineJob startJob(String scriptName, Map<String,? extends Iterable<String>> options)
throws IllegalArgumentException, FileNotFoundException, URISyntaxException {
return getInstance()._startJob(scriptName, options);
}

/**
* Singleton thread safe instance of SimpleAPI.
*/
private static SimpleAPI INSTANCE;

/**
* Get the singleton {@link SimpleAPI} instance.
*/
private static SimpleAPI getInstance() {
if (INSTANCE == null) {
for (CreateOnStart o : ServiceLoader.load(CreateOnStart.class))
Expand All @@ -111,39 +123,6 @@ private static SimpleAPI getInstance() {
return INSTANCE;
}

private static Job.Status lastJobStatus = null;
private static synchronized void updateJobStatus(Job.Status status) {
lastJobStatus = status;
}
private static List<Message> messagesQueue = new ArrayList<>();
private static int lastMessage = -1;
private static synchronized void consumeMessage(MessageAccessor accessor, int seqNum) {
for (Message m :
accessor.createFilter()
.greaterThan(lastMessage)
.filterLevels(Collections.singleton(Level.INFO))
.getMessages()) {
if (m.getSequence() > lastMessage) {
messagesQueue.add(m);
}
}
lastMessage = seqNum;
}

/**
* Get the list of new top-level messages (messages that have not
* been returned yet by a previous call to {@link #getNewMessages()}).
*/
public static synchronized List<Message> getNewMessages() {
List<Message> result = List.copyOf(messagesQueue);
messagesQueue.clear();
return result;
}

public static synchronized Job.Status getLastJobStatus() {
return lastJobStatus;
}

/**
* Simple command line interface
*/
Expand Down Expand Up @@ -171,20 +150,21 @@ public static void main(String[] args) throws InterruptedException, IOException
}
list.add(args[i + 1]);
}
CommandLineJob job = null;
try {
SimpleAPI.startJob(script, options);
job = SimpleAPI.startJob(script, options);
} catch (IllegalArgumentException e) {
System.err.println(e.getMessage());
System.exit(1);
} catch (FileNotFoundException e) {
} catch (FileNotFoundException|URISyntaxException e) {
System.err.println("File does not exist: " + e.getMessage());
System.exit(1);
}
while (true) {
for (Message m : SimpleAPI.getNewMessages()) {
for (Message m : job.getNewMessages()) {
System.err.println(m.getText());
}
switch (SimpleAPI.getLastJobStatus()) {
switch (job.getStatus()) {
case SUCCESS:
case FAIL:
case ERROR:
Expand All @@ -197,6 +177,9 @@ public static void main(String[] args) throws InterruptedException, IOException
}
}

/**
* Builder class to create a {@link CommandLineJob} object by parsing command line arguments.
*/
private static class CommandLineJobParser {

private final Script script;
Expand All @@ -214,7 +197,10 @@ public CommandLineJobParser(Script script, File fileBase) {
/**
* Parse command line argument
*/
public CommandLineJobParser withArgument(String key, String value) throws IllegalArgumentException, FileNotFoundException {
public CommandLineJobParser withArgument(String key, String value)
throws IllegalArgumentException, FileNotFoundException, URISyntaxException {
if (value == null)
throw new IllegalArgumentException();
if (script.getInputPort(key) != null)
return withInput(key, value);
else if (script.getOption(key) != null)
Expand Down Expand Up @@ -256,13 +242,22 @@ private CommandLineJobParser withInput(String port, String source) throws Illega
* the value is not valid according to the option type.
* @throws FileNotFoundException if the option type is "anyFileURI" and the value can not be
* resolved to a document.
* @throws URISyntaxException if the option type is "anyFileURI" or "anyDirURI" and the
* value starts with "file:/" but is an invalid URI
*/
private CommandLineJobParser withOption(String name, String value) throws IllegalArgumentException, FileNotFoundException {
private CommandLineJobParser withOption(String name, String value)
throws IllegalArgumentException, FileNotFoundException, URISyntaxException {
ScriptOption o = script.getOption(name);
if (o != null) {
String type = o.getType().getId();
if ("anyFileURI".equals(type)) {
File file = new File(value);
File file; {
if (value.startsWith("file:/")) {
file = new File(new URI(value));
} else {
file = new File(value);
}
}
if (!file.isAbsolute()) {
if (fileBase == null)
throw new FileNotFoundException("File must be an absolute path, but got " + file);
Expand All @@ -274,7 +269,13 @@ private CommandLineJobParser withOption(String name, String value) throws Illega
throw new UncheckedIOException(e);
}
} else if ("anyDirURI".equals(type)) {
File dir = new File(value);
File dir; {
if (value.startsWith("file:/")) {
dir = new File(new URI(value));
} else {
dir = new File(value);
}
}
if (!dir.isAbsolute()) {
if (fileBase == null)
throw new FileNotFoundException("File must be an absolute path, but got " + dir);
Expand Down Expand Up @@ -334,7 +335,7 @@ else if (file.list().length > 0)
if (file.isDirectory()) {
if (file.list().length > 0)
throw new IllegalArgumentException("Directory is not empty: " + file);
resultLocations.put(port, URI.create(file.toURI() + "/"));
resultLocations.put(port, file.toURI());
} else {
if (p.isSequence())
throw new IllegalArgumentException("Not a directory: " + file);
Expand Down Expand Up @@ -368,8 +369,18 @@ public static class CommandLineJob implements Runnable, AutoCloseable {
private CommandLineJob(Job job, Map<String,URI> resultLocations) {
this.job = job;
this.resultLocations = resultLocations;
// Simplify monitoring of messages
MessageAccessor accessor = job.getMonitor().getMessageAccessor();
accessor.listen(
num -> {
consumeMessage(accessor, num);
}
);
}

/**
* Run the job and store the results
*/
public void run() {
job.run();
try {
Expand All @@ -383,7 +394,7 @@ public void run() {
File f = new File(u);
if (u.toString().endsWith("/"))
for (JobResult r : job.getResults().getResults(port)) {
File dest = new File(f, r.strip().getIdx());
File dest = new File(f, URLDecoder.decode(r.strip().getIdx(), StandardCharsets.UTF_8));
if (dest.exists())
existingFiles.add(dest);
else
Expand All @@ -407,6 +418,9 @@ public void run() {
completed.set(true);
}

/**
* Get the current status
*/
public Job.Status getStatus() {
Job.Status s = job.getStatus();
switch (s) {
Expand All @@ -421,6 +435,52 @@ public Job.Status getStatus() {
}
}

private final List<Message> messagesQueue = new ArrayList<>();
private int lastMessage = -1;

/**
* Fill the message buffer queue for logging. The queue is returned and emptied on each
* {@link #getNewMessages()} call.
*
* @param accessor the job's {@link MessageAccessor}
* @param seqNum see {@link MessageAccessor#listen()}
*/
private synchronized void consumeMessage(MessageAccessor accessor, int seqNum) {
for (Message m :
accessor.createFilter()
.greaterThan(lastMessage)
.filterLevels(Collections.singleton(Level.INFO))
.getMessages()) {
if (m.getSequence() > lastMessage) {
messagesQueue.add(m);
}
}
lastMessage = seqNum;
}

/**
* Get the list of new top-level messages (messages that have not been returned yet by a
* previous call to {@link #getNewMessages()}).
*/
public synchronized List<Message> getNewMessages() {
List<Message> result = List.copyOf(messagesQueue);
messagesQueue.clear();
return result;
}

/**
* Get the list of all error messages reported during the job execution.
*/
public List<Message> getErrors() {
return job.getMonitor().getMessageAccessor().getErrors();
}

/**
* For advanced job monitoring, get the job's {@link JobMonitor}. From this object, you can
* access all messages reported for the job through {@link JobMonitor#getMessageAccessor()},
* or register your own status notifications callback through {@link
* JobMonitor#getStatusUpdates()}.
*/
public JobMonitor getMonitor() {
return job.getMonitor();
}
Expand Down
6 changes: 3 additions & 3 deletions src/main/resources/README.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
DAISY Pipeline 2 - 1.14.17 - January 22, 2024
DAISY Pipeline 2 - 1.14.19 - July 12, 2024
==============================================================================


Expand Down Expand Up @@ -72,10 +72,10 @@ The package includes:
3. Release Notes
------------------------------------------------------------------------------

The package includes the 1.14.17 version of the project.
The package includes the 1.14.19 version of the project.

See the release notes on this page:
https://github.com/daisy/pipeline-assembly/blob/master/NEWS.md#changes-in-release-v11417
https://github.com/daisy/pipeline-assembly/blob/master/NEWS.md#changes-in-release-v11419

4. Prerequisites
------------------------------------------------------------------------------
Expand Down

0 comments on commit ff701bc

Please sign in to comment.