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