1 package org.daisy.pipeline.braille.liblouis.impl;
2
3 import java.io.ByteArrayInputStream;
4 import java.io.File;
5 import java.io.FileOutputStream;
6 import java.io.InputStream;
7 import java.io.IOException;
8 import java.net.URI;
9 import java.net.URL;
10 import java.nio.charset.StandardCharsets;
11 import java.nio.file.Files;
12 import static java.nio.file.Files.createTempDirectory;
13 import java.text.Normalizer;
14 import java.util.HashMap;
15 import java.util.Locale;
16 import java.util.Map;
17 import java.util.NoSuchElementException;
18 import java.util.Set;
19
20 import com.google.common.base.Function;
21 import static com.google.common.base.Functions.toStringFunction;
22 import com.google.common.base.MoreObjects;
23 import com.google.common.base.MoreObjects.ToStringHelper;
24 import static com.google.common.collect.Iterables.transform;
25 import static com.google.common.collect.Sets.newHashSet;
26
27 import org.daisy.common.file.URLs;
28 import org.daisy.pipeline.braille.common.AbstractTransformProvider;
29 import org.daisy.pipeline.braille.common.AbstractTransformProvider.util.Iterables;
30 import static org.daisy.pipeline.braille.common.AbstractTransformProvider.util.logSelect;
31 import static org.daisy.pipeline.braille.common.AbstractTransformProvider.util.warn;
32 import org.daisy.pipeline.braille.common.NativePath;
33 import org.daisy.pipeline.braille.common.Query;
34 import org.daisy.pipeline.braille.common.Query.Feature;
35 import org.daisy.pipeline.braille.common.Query.MutableQuery;
36 import static org.daisy.pipeline.braille.common.Query.util.mutableQuery;
37 import org.daisy.pipeline.braille.common.Transform;
38 import org.daisy.pipeline.braille.common.TransformProvider;
39 import static org.daisy.pipeline.braille.common.TransformProvider.util.varyLocale;
40 import static org.daisy.pipeline.braille.common.util.Files.unpack;
41 import static org.daisy.pipeline.braille.common.util.Files.asFile;
42 import static org.daisy.pipeline.braille.common.util.Files.normalize;
43 import static org.daisy.pipeline.braille.common.util.Locales.parseLocale;
44 import static org.daisy.pipeline.braille.common.util.Strings.join;
45 import org.daisy.pipeline.braille.common.WithSideEffect;
46 import org.daisy.pipeline.braille.liblouis.LiblouisTable;
47
48 import org.liblouis.CompilationException;
49 import org.liblouis.DisplayTable;
50 import org.liblouis.DisplayTable.Fallback;
51 import org.liblouis.Louis;
52 import org.liblouis.Table;
53 import org.liblouis.TableInfo;
54 import org.liblouis.TableResolver;
55 import org.liblouis.Translator;
56
57 import org.osgi.service.component.annotations.Activate;
58 import org.osgi.service.component.annotations.Component;
59 import org.osgi.service.component.annotations.Deactivate;
60 import org.osgi.service.component.annotations.Reference;
61 import org.osgi.service.component.annotations.ReferenceCardinality;
62 import org.osgi.service.component.annotations.ReferencePolicy;
63
64 import org.slf4j.Logger;
65 import org.slf4j.LoggerFactory;
66
67 @Component(
68 name = "org.daisy.pipeline.braille.liblouis.impl.LiblouisTableJnaImplProvider",
69 service = {
70 LiblouisTableJnaImplProvider.class
71 }
72 )
73 public class LiblouisTableJnaImplProvider extends AbstractTransformProvider<LiblouisTableJnaImplProvider.LiblouisTableJnaImpl> {
74
75
76 public class LiblouisTableJnaImpl extends LiblouisTable implements Transform {
77
78 private final Translator translator;
79 private final DisplayTable displayTable;
80 private final TableInfo info;
81
82 LiblouisTableJnaImpl(Translator translator, DisplayTable displayTable, TableInfo info) {
83 super(translator.getTable());
84 this.translator = translator;
85 this.displayTable = displayTable;
86 this.info = info;
87 }
88
89 public Translator getTranslator() {
90 return translator;
91 }
92
93 public DisplayTable getDisplayTable() {
94 return displayTable;
95 }
96
97 public String getIdentifier() {
98 return toString();
99 }
100
101 public String getDisplayName() {
102 return info != null ? info.get("display-name") : null;
103 }
104
105 public Normalizer.Form getUnicodeNormalizationForm() {
106 if (info != null) {
107 String form = info.get("unicode-form");
108 if (form != null)
109 try {
110 return Normalizer.Form.valueOf(form.toUpperCase());
111 } catch (IllegalArgumentException e) {}}
112 return null;
113 }
114
115 @Override
116 public String toString() {
117 return MoreObjects.toStringHelper("LiblouisTableJnaImpl")
118 .add("translator", super.toString())
119 .add("displayTable", displayTable)
120 .toString();
121 }
122 }
123
124 private LiblouisTableRegistry tableRegistry;
125
126 private DisplayTable unicodeDisplayTable;
127 private DisplayTable unicodeDisplayTableWithNoBreakSpace;
128 private File spacesFile;
129 private File spacesDisFile;
130 private File tempDir;
131
132 private void registerTableResolver() {
133 Louis.setTableResolver(new TableResolver() {
134 private final Map<String,URL> aggregatorTables = new HashMap<String,URL>();
135 @Override
136 public URL resolve(String table, URL base) {
137 logger.debug("Resolving " + table + (base != null ? " against base " + base : ""));
138
139 if (aggregatorTables.containsValue(base))
140 base = null;
141 File baseFile = base == null ? null : asFile(base);
142 File[] resolved = tableRegistry.resolveLiblouisTable(new LiblouisTable(table), baseFile);
143 if (resolved != null) {
144 logger.debug("Resolved to " + join(resolved, ","));
145 if (resolved.length == 1)
146 return URLs.asURL(resolved[0]);
147 else {
148
149 if (aggregatorTables.containsKey(table)) {
150 URL u = aggregatorTables.get(table);
151 logger.debug("... aggregated into " + u);
152 return u;
153 }
154 try {
155 StringBuilder b = new StringBuilder();
156 for (File f : resolved)
157 b.append("include ").append(URLs.asURI(f.getCanonicalFile()).toASCIIString()).append('\n');
158 InputStream in = new ByteArrayInputStream(b.toString().getBytes(StandardCharsets.UTF_8));
159 File f = File.createTempFile("aggregator-", ".tbl", tempDir);
160 f.deleteOnExit();
161 f.delete();
162 Files.copy(in, f.toPath());
163 f = f.getCanonicalFile();
164 URL u = URLs.asURL(f);
165 aggregatorTables.put(table, u);
166 logger.debug("... aggregated into " + u);
167 return u;
168 } catch (IOException e) {
169 throw new RuntimeException(e);
170 }
171 }
172 }
173 logger.error("Table could not be resolved");
174 return null;
175 }
176 @Override
177 public Set<String> list() {
178 return newHashSet(
179 transform(
180 tableRegistry.listAllTableFiles(),
181 toStringFunction()));
182 }
183 }
184 );
185 }
186
187
188
189 @Activate
190 protected void activate() {
191 logger.debug("Loading liblouis service");
192 try {
193 tempDir = normalize(createTempDirectory("pipeline-").toFile());
194 tempDir.deleteOnExit();
195 } catch (Exception e) {
196 throw new RuntimeException("Could not create temporary directory", e);
197 }
198 try {
199 tableRegistry.onPathChange(
200 new Function<LiblouisTableRegistry,Void>() {
201 public Void apply(LiblouisTableRegistry r) {
202
203 registerTableResolver();
204 invalidateCache();
205 return null; }});
206 registerTableResolver();
207
208 logger.debug("liblouis version: {}", Louis.getVersion());
209 File unicodeDisFile = new File(tempDir, "unicode.dis");
210 unpack(
211 URLs.getResourceFromJAR("/tables/unicode.dis", LiblouisTableJnaImplProvider.class),
212 unicodeDisFile);
213 unicodeDisFile.deleteOnExit();
214 unicodeDisplayTable = DisplayTable.fromTable("" + URLs.asURI(unicodeDisFile), Fallback.MASK);
215 spacesFile = new File(tempDir, "spaces.cti");
216 unpack(
217 URLs.getResourceFromJAR("/tables/spaces.cti", LiblouisTableJnaImplProvider.class),
218 spacesFile);
219 spacesFile.deleteOnExit();
220 spacesDisFile = new File(tempDir, "spaces.dis");
221 unpack(
222 URLs.getResourceFromJAR("/tables/spaces.dis", LiblouisTableJnaImplProvider.class),
223 spacesDisFile);
224 spacesDisFile.deleteOnExit();
225 unicodeDisplayTableWithNoBreakSpace = DisplayTable.fromTable(
226 "" + URLs.asURI(unicodeDisFile) + "," + URLs.asURI(spacesDisFile), Fallback.MASK);
227 Louis.setLogger(new org.liblouis.Logger() {
228 @Override
229 public void log(Level level, String message) {
230 switch (level) {
231 case ALL: logger.trace(message); break;
232 case DEBUG: logger.debug(message); break;
233 case INFO: logger.debug("INFO: " + message); break;
234 case WARN: logger.debug("WARN: " + message); break;
235
236 case ERROR: logger.error(message); break;
237 case FATAL: logger.error(message); break; }}}); }
238 catch (Throwable e) {
239 logger.error("liblouis service could not be loaded", e);
240 throw e; }
241 }
242
243 @Deactivate
244 protected void deactivate() {
245 logger.debug("Unloading liblouis service");
246 }
247
248 @Reference(
249 name = "LiblouisLibrary",
250 unbind = "-",
251 service = NativePath.class,
252 target = "(identifier=http://www.liblouis.org/native/*)",
253 cardinality = ReferenceCardinality.MANDATORY,
254 policy = ReferencePolicy.STATIC
255 )
256 protected void bindLibrary(NativePath path) {
257 if (LiblouisExternalNativePath.LIBLOUIS_EXTERNAL)
258 logger.info("Using external liblouis");
259 else {
260 URI libraryPath = path.get("liblouis").iterator().next();
261 Louis.setLibraryPath(asFile(path.resolve(libraryPath)));
262 logger.debug("Registering liblouis library: " + libraryPath); }
263 }
264
265 @Reference(
266 name = "LiblouisTableRegistry",
267 unbind = "-",
268 service = LiblouisTableRegistry.class,
269 cardinality = ReferenceCardinality.MANDATORY,
270 policy = ReferencePolicy.STATIC
271 )
272 protected void bindTableRegistry(LiblouisTableRegistry registry) {
273 tableRegistry = registry;
274 logger.debug("Registering Liblouis table registry: " + registry);
275 }
276
277 public Iterable<LiblouisTableJnaImpl> _get(Query query) {
278 return logSelect(query, _provider);
279 }
280
281 @Override
282 public ToStringHelper toStringHelper() {
283 return MoreObjects.toStringHelper("LiblouisTableJnaImplProvider");
284 }
285
286 private TransformProvider<LiblouisTableJnaImpl> _provider
287 = varyLocale(
288 new AbstractTransformProvider<LiblouisTableJnaImpl>() {
289 public Iterable<LiblouisTableJnaImpl> _get(final Query query) {
290 return Iterables.of(
291 new WithSideEffect<LiblouisTableJnaImpl,Logger>() {
292 public LiblouisTableJnaImpl _apply() {
293 MutableQuery q = mutableQuery(query);
294 String table = null;
295 String charset = null;
296 TableInfo tableInfo = null;
297 boolean whiteSpace = false;
298 String dotsForUndefinedChar = null;
299 Locale documentLocale = null;
300 if (q.containsKey("white-space")) {
301 q.removeOnly("white-space");
302 whiteSpace = true; }
303 if (q.containsKey("dots-for-undefined-char")) {
304 dotsForUndefinedChar = q.removeOnly("dots-for-undefined-char").getValue().get();
305 if (!dotsForUndefinedChar.matches("[\u2800-\u28FF]+")) {
306 logger.warn(dotsForUndefinedChar + " is not a valid dot pattern string.");
307 throw new NoSuchElementException();
308 }
309 }
310 if (q.containsKey("document-locale"))
311 documentLocale = parseLocale(q.removeOnly("document-locale").getValue().get());
312 if (q.containsKey("charset") || q.containsKey("braille-charset"))
313 charset = q.containsKey("charset")
314 ? q.removeOnly("charset").getValue().get()
315 : q.removeOnly("braille-charset").getValue().get();
316 if (q.containsKey("table") || q.containsKey("liblouis-table")) {
317 table = q.containsKey("table")
318 ? q.removeOnly("table").getValue().get()
319 : q.removeOnly("liblouis-table").getValue().get();
320 tableInfo = new TableInfo(table);
321 if (q.containsKey("locale")) {
322
323 String locale = q.removeOnly("locale").getValue().get();
324 q.add("language", locale);
325 q.add("region", locale);
326 }
327 for (Feature f : q)
328 if (!f.getValue().orElse("yes").equals(tableInfo.get(f.getKey()))) {
329 logger.warn("Table " + table + " does not match " + f);
330 throw new NoSuchElementException(); }
331 } else {
332 if (documentLocale != null && !q.containsKey("locale")) {
333
334
335
336 if (!documentLocale.equals(new Locale(documentLocale.getLanguage(), documentLocale.getCountry()))) {
337
338
339 if (!q.containsKey("language"))
340 q.add("language", documentLocale.toLanguageTag());
341 } else if (q.containsKey("region")) {
342
343
344 if (!q.containsKey("language"))
345 q.add("language", documentLocale.getLanguage());
346 } else {
347
348
349
350 if (!q.containsKey("language"))
351 q.add("language", documentLocale.getLanguage());
352 if (!"".equals(documentLocale.getCountry()))
353 q.add("region", documentLocale.toLanguageTag());
354 }
355 }
356 if (q.isEmpty())
357 throw new NoSuchElementException();
358 StringBuilder b = new StringBuilder();
359
360
361
362
363
364
365
366
367
368 for (Feature f : q) {
369 String k = f.getKey();
370 if (!k.matches("[a-zA-Z0-9_-]+")) {
371 __apply(
372 warn("Invalid syntax for feature key: " + k));
373 throw new NoSuchElementException(); }
374 b.append(k);
375 if (f.hasValue()) {
376 String v = f.getValue().get();
377 if (!v.matches("[a-zA-Z0-9_-]+")) {
378 __apply(
379 warn("Invalid syntax for feature value: " + v));
380 throw new NoSuchElementException(); }
381 b.append(":" + v); }
382 b.append(" "); }
383 try {
384 Table t = Table.find(b.toString());
385 table = t.getIdentifier();
386 tableInfo = t.getInfo(); }
387 catch (IllegalArgumentException e) {}
388 catch (NoSuchElementException e) {}}
389 if (table != null) {
390 if (whiteSpace)
391 table = URLs.asURI(spacesFile) + "," + table;
392 DisplayTable displayTable = null;
393 if (charset == null)
394 displayTable = whiteSpace ? unicodeDisplayTableWithNoBreakSpace : unicodeDisplayTable;
395 else
396 try {
397 if (whiteSpace)
398 charset = "" + URLs.asURI(spacesDisFile) + "," + charset;
399
400
401 displayTable = new Translator(charset).asDisplayTable(); }
402 catch (CompilationException e) {
403
404 throw new NoSuchElementException(); }
405 if (dotsForUndefinedChar != null) {
406 try {
407 File undefinedFile = File.createTempFile("undefined-", ".uti", tempDir);
408 undefinedFile.deleteOnExit();
409 undefinedFile.createNewFile();
410 FileOutputStream writer = new FileOutputStream(undefinedFile);
411 String dotPattern; {
412 StringBuilder b = new StringBuilder();
413 for (char c : dotsForUndefinedChar.toCharArray()) {
414 b.append("-");
415 c &= (char)0xFF;
416 if (c == 0)
417 b.append("0");
418 else
419 for (int k = 1; k <= 8; k++) {
420 if ((c & (char)1) != 0)
421 b.append(k);
422 c = (char)(c >> 1); }}
423 dotPattern = b.toString().substring(1); }
424 writer.write(("undefined " + dotPattern + "\n").getBytes());
425 writer.flush();
426 writer.close();
427
428 table = table + "," + URLs.asURI(undefinedFile);
429 } catch (IOException e) {
430 throw new RuntimeException(e);
431 }
432 }
433 try {
434 return new LiblouisTableJnaImpl(new Translator(table), displayTable, tableInfo); }
435 catch (CompilationException e) {
436 __apply(
437 warn("Could not compile table " + table));
438 logger.warn("Could not compile table", e); }}
439 throw new NoSuchElementException();
440 }
441 }
442 );
443 }
444 }
445 );
446
447 private static final Logger logger = LoggerFactory.getLogger(LiblouisTableJnaImplProvider.class);
448
449 }