All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.daisy.pipeline.braille.liblouis.impl.LiblouisTableJnaImplProvider Maven / Gradle / Ivy

There is a newer version: 6.3.0
Show newest version
package org.daisy.pipeline.braille.liblouis.impl;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import static java.nio.file.Files.createTempDirectory;
import java.text.Normalizer;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

import com.google.common.base.Function;
import static com.google.common.base.Functions.toStringFunction;
import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Sets.newHashSet;

import org.daisy.common.file.URLs;
import org.daisy.pipeline.braille.common.AbstractTransformProvider;
import org.daisy.pipeline.braille.common.AbstractTransformProvider.util.Iterables;
import static org.daisy.pipeline.braille.common.AbstractTransformProvider.util.logSelect;
import static org.daisy.pipeline.braille.common.AbstractTransformProvider.util.warn;
import org.daisy.pipeline.braille.common.NativePath;
import org.daisy.pipeline.braille.common.Query;
import org.daisy.pipeline.braille.common.Query.Feature;
import org.daisy.pipeline.braille.common.Query.MutableQuery;
import static org.daisy.pipeline.braille.common.Query.util.mutableQuery;
import org.daisy.pipeline.braille.common.Transform;
import org.daisy.pipeline.braille.common.TransformProvider;
import static org.daisy.pipeline.braille.common.TransformProvider.util.varyLocale;
import static org.daisy.pipeline.braille.common.util.Files.unpack;
import static org.daisy.pipeline.braille.common.util.Files.asFile;
import static org.daisy.pipeline.braille.common.util.Files.normalize;
import static org.daisy.pipeline.braille.common.util.Locales.parseLocale;
import static org.daisy.pipeline.braille.common.util.Strings.join;
import org.daisy.pipeline.braille.common.WithSideEffect;
import org.daisy.pipeline.braille.liblouis.LiblouisTable;

import org.liblouis.CompilationException;
import org.liblouis.DisplayTable;
import org.liblouis.DisplayTable.Fallback;
import org.liblouis.Louis;
import org.liblouis.Table;
import org.liblouis.TableInfo;
import org.liblouis.TableResolver;
import org.liblouis.Translator;

import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component(
	name = "org.daisy.pipeline.braille.liblouis.impl.LiblouisTableJnaImplProvider",
	service = {
		LiblouisTableJnaImplProvider.class
	}
)
public class LiblouisTableJnaImplProvider extends AbstractTransformProvider {

	// FIXME: isn't really a Transform but implements it so that we can use TransformProvider
	public class LiblouisTableJnaImpl extends LiblouisTable implements Transform {
		
		private final Translator translator;
		private final DisplayTable displayTable;
		private final TableInfo info;
		
		LiblouisTableJnaImpl(Translator translator, DisplayTable displayTable, TableInfo info) {
			super(translator.getTable());
			this.translator = translator;
			this.displayTable = displayTable;
			this.info = info;
		}
		
		public Translator getTranslator() {
			return translator;
		}
		
		public boolean usesCustomDisplayTable() {
			return displayTable != unicodeDisplayTable
				&& displayTable != unicodeDisplayTableWithNoBreakSpace;
		}
		
		public DisplayTable getDisplayTable() {
			return displayTable;
		}
		
		public String getIdentifier() {
			return toString();
		}
		
		public String getDisplayName() {
			return info != null ? info.get("display-name") : null;
		}
		
		public Normalizer.Form getUnicodeNormalizationForm() {
			if (info != null) {
				String form = info.get("unicode-form");
				if (form != null)
					try {
						return Normalizer.Form.valueOf(form.toUpperCase());
					} catch (IllegalArgumentException e) {}}
			return null;
		}

		@Override
		public String toString() {
			return MoreObjects.toStringHelper("LiblouisTableJnaImpl")
			                  .add("translator", super.toString())
			                  .add("displayTable", displayTable)
			                  .toString();
		}
	}
	
	private LiblouisTableRegistry tableRegistry;
	
	private DisplayTable unicodeDisplayTable;
	private DisplayTable unicodeDisplayTableWithNoBreakSpace;
	private File spacesFile;
	private File spacesDisFile;
	private File tempDir;
	
	private void registerTableResolver() {
		Louis.setTableResolver(new TableResolver() {
				private final Map aggregatorTables = new HashMap();
				@Override
				public URL resolve(String table, URL base) {
					logger.debug("Resolving " + table + (base != null ? " against base " + base : ""));
					// if we are resolving an include rule from a generated aggregator table, resolve without base
					if (aggregatorTables.containsValue(base))
						base = null;
					File baseFile = base == null ? null : asFile(base); // base is expected to be a file
					File[] resolved = tableRegistry.resolveLiblouisTable(new LiblouisTable(table), baseFile);
					if (resolved != null) {
						logger.debug("Resolved to " + join(resolved, ","));
						if (resolved.length == 1)
							return URLs.asURL(resolved[0]);
						else {
							// if it is a comma separated table list, create a single file that includes all the sub-tables
							if (aggregatorTables.containsKey(table)) {
								URL u = aggregatorTables.get(table);
								logger.debug("... aggregated into " + u);
								return u;
							}
							try {
								StringBuilder b = new StringBuilder();
								for (File f : resolved)
									b.append("include ").append(URLs.asURI(f.getCanonicalFile()).toASCIIString()).append('\n');
								InputStream in = new ByteArrayInputStream(b.toString().getBytes(StandardCharsets.UTF_8));
								File f = File.createTempFile("aggregator-", ".tbl", tempDir);
								f.deleteOnExit();
								f.delete();
								Files.copy(in, f.toPath());
								f = f.getCanonicalFile();
								URL u = URLs.asURL(f);
								aggregatorTables.put(table, u);
								logger.debug("... aggregated into " + u);
								return u;
							} catch (IOException e) {
								throw new RuntimeException(e); // should not happen
							}
						}
					}
					logger.debug("Table could not be resolved");
					return null;
				}
				@Override
				public Set list() {
					return newHashSet(
						transform(
							tableRegistry.listAllTableFiles(),
							toStringFunction()));
				}
			}
		);
	}
	
	// WARNING: only one instance of LiblouisTableJnaImplProvider should be created because
	// setLibraryPath, setTableResolver and setLogger are global functions
	@Activate
	protected void activate() {
		logger.debug("Loading liblouis service");
		try {
			tempDir = normalize(createTempDirectory("pipeline-").toFile());
			tempDir.deleteOnExit();
		} catch (Exception e) {
			throw new RuntimeException("Could not create temporary directory", e);
		}
		try {
			tableRegistry.onPathChange(
				new Function() {
					public Void apply(LiblouisTableRegistry r) {
						// re-register table resolver so that liblouis-java re-indexes tables
						registerTableResolver();
						invalidateCache();
						return null; }});
			registerTableResolver();
			// invoke after table resolver registered because otherwise default tables will be unpacked for no reason
			logger.debug("liblouis version: {}", Louis.getVersion());
			File unicodeDisFile = new File(tempDir, "unicode.dis");
			unpack(
				URLs.getResourceFromJAR("/tables/unicode.dis", LiblouisTableJnaImplProvider.class),
				unicodeDisFile);
			unicodeDisFile.deleteOnExit();
			unicodeDisplayTable = DisplayTable.fromTable("" + URLs.asURI(unicodeDisFile), Fallback.MASK);
			spacesFile = new File(tempDir, "spaces.cti");
			unpack(
				URLs.getResourceFromJAR("/tables/spaces.cti", LiblouisTableJnaImplProvider.class),
				spacesFile);
			spacesFile.deleteOnExit();
			spacesDisFile = new File(tempDir, "spaces.dis");
			unpack(
				URLs.getResourceFromJAR("/tables/spaces.dis", LiblouisTableJnaImplProvider.class),
				spacesDisFile);
			spacesDisFile.deleteOnExit();
			unicodeDisplayTableWithNoBreakSpace = DisplayTable.fromTable(
				"" + URLs.asURI(unicodeDisFile) + "," + URLs.asURI(spacesDisFile), Fallback.MASK);
			Louis.setLogger(new org.liblouis.Logger() {
					@Override
					public void log(Level level, String message) {
						switch (level) {
						case ALL: logger.trace(message); break;
						case DEBUG: logger.debug(message); break;
						case INFO: logger.debug("INFO: " + message); break;
						case WARN: logger.debug("WARN: " + message); break;
						case ERROR:
						case FATAL:
							// ignore errors, they will be included in the exception
							break; }}}); }
		catch (Throwable e) {
			logger.error("liblouis service could not be loaded", e);
			throw e; }
	}
	
	@Deactivate
	protected void deactivate() {
		logger.debug("Unloading liblouis service");
	}
	
	@Reference(
		name = "LiblouisLibrary",
		unbind = "-",
		service = NativePath.class,
		target = "(identifier=http://www.liblouis.org/native/*)",
		cardinality = ReferenceCardinality.MANDATORY,
		policy = ReferencePolicy.STATIC
	)
	protected void bindLibrary(NativePath path) {
		if (LiblouisExternalNativePath.LIBLOUIS_EXTERNAL)
			logger.info("Using external liblouis");
		else {
			URI libraryPath = path.get("liblouis").iterator().next();
			Louis.setLibraryPath(asFile(path.resolve(libraryPath)));
			logger.debug("Registering liblouis library: " + libraryPath); }
	}
	
	@Reference(
		name = "LiblouisTableRegistry",
		unbind = "-",
		service = LiblouisTableRegistry.class,
		cardinality = ReferenceCardinality.MANDATORY,
		policy = ReferencePolicy.STATIC
	)
	protected void bindTableRegistry(LiblouisTableRegistry registry) {
		tableRegistry = registry;
		logger.debug("Registering Liblouis table registry: " + registry);
	}
	
	public Iterable _get(Query query) {
		return logSelect(query, _provider);
	}
	
	@Override
	public ToStringHelper toStringHelper() {
		return MoreObjects.toStringHelper("LiblouisTableJnaImplProvider");
	}
	
	private TransformProvider _provider
	= varyLocale(
		new AbstractTransformProvider() {
			public Iterable _get(final Query query) {
				return Iterables.of(
					new WithSideEffect() {
						public LiblouisTableJnaImpl _apply() {
							MutableQuery q = mutableQuery(query);
							String table = null;
							String charset = null;
							TableInfo tableInfo = null;
							boolean whiteSpace = false;
							String dotsForUndefinedChar = null;
							Locale documentLocale = null;
							if (q.containsKey("white-space")) {
								q.removeOnly("white-space");
								whiteSpace = true; }
							if (q.containsKey("dots-for-undefined-char")) {
								dotsForUndefinedChar = q.removeOnly("dots-for-undefined-char").getValue().get();
								if (!dotsForUndefinedChar.matches("[\u2800-\u28FF]+")) {
									logger.warn(dotsForUndefinedChar + " is not a valid dot pattern string.");
									throw new NoSuchElementException();
								}
							}
							if (q.containsKey("document-locale"))
								documentLocale = parseLocale(q.removeOnly("document-locale").getValue().get());
							if (q.containsKey("charset") || q.containsKey("braille-charset"))
								charset = q.containsKey("charset")
									? q.removeOnly("charset").getValue().get()
									: q.removeOnly("braille-charset").getValue().get();
							if (q.containsKey("table") || q.containsKey("liblouis-table")) {
								table = q.containsKey("table")
									? q.removeOnly("table").getValue().get()
									: q.removeOnly("liblouis-table").getValue().get();
								tableInfo = new TableInfo(table);
								if (q.containsKey("locale")) {
									// locale is shorthand for language + region
									String locale = q.removeOnly("locale").getValue().get();
									q.add("language", locale);
									q.add("region", locale);
								}
								for (Feature f : q)
									if (!f.getValue().orElse("yes").equals(tableInfo.get(f.getKey()))) {
										logger.warn("Table " + table + " does not match " + f);
										throw new NoSuchElementException(); }
							} else {
								if (documentLocale != null && !q.containsKey("locale")) {
									// Liblouis table selection happens based on a language tag (primary target language
									// of the braille code) and an optional region tag (region or community in which the
									// braille code applies).
									if (!documentLocale.equals(new Locale(documentLocale.getLanguage(), documentLocale.getCountry()))) {
										// If the document locale has other subtags than language and region, we
										// interpret the locale as a language.
										if (!q.containsKey("language"))
											q.add("language", documentLocale.toLanguageTag());
									} else if (q.containsKey("region")) {
										// If the region is already specified in the query, we ignore the region subtag
										// of the document locale.
										if (!q.containsKey("language"))
											q.add("language", documentLocale.getLanguage());
									} else {
										// Otherwise we use the language subtag of the document locale as the language,
										// and the region subtag (if specified) as the region in which the braille code
										// applies.
										if (!q.containsKey("language"))
											q.add("language", documentLocale.getLanguage());
										if (!"".equals(documentLocale.getCountry()))
											q.add("region", documentLocale.toLanguageTag());
									}
								}
								if (q.isEmpty())
									throw new NoSuchElementException();
								StringBuilder b = new StringBuilder();
								
								// FIXME: if query does not contain (type:display), need to match for absence of "type:display"
								// -> i.e. Liblouis query syntax must support negation!
								// -> this used to be solved by matching "type:translation" but the downside of this
								//    is that this feature had to be added to every table which is not desired
								// -> another solution would be to let Liblouis return a list of possible matches and
								//    select the first match that does not end with ".dis" (or that does not have the
								//    feature "type:display")
								
								for (Feature f : q) {
									String k = f.getKey();
									if (!k.matches("[a-zA-Z0-9_-]+")) {
										__apply(
											warn("Invalid syntax for feature key: " + k));
										throw new NoSuchElementException(); }
									b.append(k);
									if (f.hasValue()) {
										String v = f.getValue().get();
										if (!v.matches("[a-zA-Z0-9_-]+")) {
											__apply(
												warn("Invalid syntax for feature value: " + v));
											throw new NoSuchElementException(); }
										b.append(":" + v); }
									b.append(" "); }
								try {
									Table t = Table.find(b.toString());
									table = t.getIdentifier();
									tableInfo = t.getInfo(); }
								catch (IllegalArgumentException e) {}
								catch (NoSuchElementException e) {}}
							if (table != null) {
								if (whiteSpace)
									table = URLs.asURI(spacesFile) + "," + table;
								DisplayTable displayTable = null;
								if (charset == null)
									displayTable = whiteSpace ? unicodeDisplayTableWithNoBreakSpace : unicodeDisplayTable;
								else
									try {
										if (whiteSpace)
											charset = "" + URLs.asURI(spacesDisFile) + "," + charset;
										// using Translator.asDisplayTable() and not DisplayTable.fromTable() so we can
										// catch CompilationException
										displayTable = new Translator(charset).asDisplayTable(); }
									catch (CompilationException e) {
										// the specified table is not a Liblouis table
										throw new NoSuchElementException(); }
								if (dotsForUndefinedChar != null) {
									try {
										File undefinedFile = File.createTempFile("undefined-", ".uti", tempDir);
										undefinedFile.deleteOnExit();
										undefinedFile.createNewFile();
										FileOutputStream writer = new FileOutputStream(undefinedFile);
										String dotPattern; {
											StringBuilder b = new StringBuilder();
											for (char c : dotsForUndefinedChar.toCharArray()) {
												b.append("-");
												c &= (char)0xFF;
												if (c == 0)
													b.append("0");
												else
													for (int k = 1; k <= 8; k++) {
														if ((c & (char)1) != 0)
															b.append(k);
														c = (char)(c >> 1); }}
											dotPattern = b.toString().substring(1); }
										writer.write(("undefined " + dotPattern + "\n").getBytes());
										writer.flush();
										writer.close();
										// adding the "undefined" rule to the end overwrites any previous rules
										table = table + "," + URLs.asURI(undefinedFile);
									} catch (IOException e) {
										throw new RuntimeException(e);
									}
								}
								try {
									return new LiblouisTableJnaImpl(new Translator(table), displayTable, tableInfo); }
								catch (CompilationException e) {
									__apply(
										warn("Could not compile table " + table));
									logger.warn("Could not compile table", e); }}
							throw new NoSuchElementException();
						}
					}
				);
			}
		}
	);
	
	private static final Logger logger = LoggerFactory.getLogger(LiblouisTableJnaImplProvider.class);
	
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy