1   package org.daisy.pipeline.braille.dotify.impl;
2   
3   import java.util.ArrayList;
4   import java.util.Arrays;
5   import java.util.Collections;
6   import java.util.HashMap;
7   import java.util.List;
8   import java.util.Locale;
9   import java.util.Map;
10  import java.util.NoSuchElementException;
11  
12  import com.google.common.base.MoreObjects;
13  import com.google.common.base.MoreObjects.ToStringHelper;
14  import com.google.common.collect.ImmutableList;
15  import static com.google.common.collect.Iterables.size;
16  
17  import cz.vutbr.web.css.CSSProperty;
18  
19  import org.daisy.braille.css.BrailleCSSProperty.Hyphens;
20  import org.daisy.braille.css.SimpleInlineStyle;
21  
22  import org.daisy.dotify.api.translator.BrailleFilter;
23  import org.daisy.dotify.api.translator.BrailleFilterFactoryService;
24  import org.daisy.dotify.api.translator.Translatable;
25  import org.daisy.dotify.api.translator.TranslationException;
26  import org.daisy.dotify.api.translator.TranslatorConfigurationException;
27  import org.daisy.dotify.api.translator.TranslatorMode;
28  import org.daisy.dotify.api.translator.TranslatorType;
29  
30  import org.daisy.pipeline.braille.common.AbstractBrailleTranslator;
31  import org.daisy.pipeline.braille.common.AbstractBrailleTranslator.util.DefaultLineBreaker;
32  import org.daisy.pipeline.braille.common.AbstractTransformProvider;
33  import org.daisy.pipeline.braille.common.AbstractTransformProvider.util.Function;
34  import org.daisy.pipeline.braille.common.AbstractTransformProvider.util.Iterables;
35  import static org.daisy.pipeline.braille.common.AbstractTransformProvider.util.Iterables.concat;
36  import static org.daisy.pipeline.braille.common.AbstractTransformProvider.util.logCreate;
37  import static org.daisy.pipeline.braille.common.AbstractTransformProvider.util.logSelect;
38  import org.daisy.pipeline.braille.common.BrailleTranslatorProvider;
39  import org.daisy.pipeline.braille.common.Hyphenator;
40  import org.daisy.pipeline.braille.common.Query;
41  import org.daisy.pipeline.braille.common.Query.Feature;
42  import org.daisy.pipeline.braille.common.Query.MutableQuery;
43  import static org.daisy.pipeline.braille.common.Query.util.mutableQuery;
44  import org.daisy.pipeline.braille.common.TransformProvider;
45  import static org.daisy.pipeline.braille.common.TransformProvider.util.varyLocale;
46  import static org.daisy.pipeline.braille.common.util.Locales.parseLocale;
47  import static org.daisy.pipeline.braille.common.util.Strings.extractHyphens;
48  import static org.daisy.pipeline.braille.common.util.Strings.join;
49  import org.daisy.pipeline.braille.css.CSSStyledText;
50  import org.daisy.pipeline.braille.css.TextStyleParser;
51  import org.daisy.pipeline.braille.dotify.DotifyTranslator;
52  
53  import org.osgi.framework.FrameworkUtil;
54  
55  import org.osgi.service.component.annotations.Component;
56  import org.osgi.service.component.annotations.Reference;
57  import org.osgi.service.component.annotations.ReferenceCardinality;
58  import org.osgi.service.component.annotations.ReferencePolicy;
59  
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  
63  /**
64   * @see <a href="../../../../../../../../../doc/">User documentation</a>.
65   */
66  public class DotifyTranslatorImpl extends AbstractBrailleTranslator implements DotifyTranslator {
67  	
68  	private final static char SHY = '\u00AD';  // soft hyphen
69  	private final static char ZWSP = '\u200B'; // zero-width space
70  	
71  	private final BrailleFilter filter;
72  	private final boolean hyphenating;
73  	private final Hyphenator externalHyphenator;
74  	private final Provider provider;
75  	
76  	private DotifyTranslatorImpl(BrailleFilter filter, boolean hyphenating, Provider provider) {
77  		this.filter = filter;
78  		this.hyphenating = hyphenating;
79  		this.externalHyphenator = null;
80  		this.provider = provider;
81  	}
82  	
83  	private DotifyTranslatorImpl(BrailleFilter filter, Hyphenator externalHyphenator, Provider provider) {
84  		super(externalHyphenator, null);
85  		this.filter = filter;
86  		this.hyphenating = true;
87  		this.externalHyphenator = externalHyphenator;
88  		this.provider = provider;
89  	}
90  	
91  	private DotifyTranslatorImpl(DotifyTranslatorImpl from, Hyphenator hyphenator) {
92  		super(from);
93  		this.filter = from.filter;
94  		this.hyphenating = hyphenator != null;
95  		this.externalHyphenator = hyphenator;
96  		this.provider = from.provider;
97  	}
98  	
99  	public BrailleFilter asBrailleFilter() {
100 		return filter;
101 	}
102 	
103 	@Override
104 	public DotifyTranslatorImpl _withHyphenator(Hyphenator hyphenator) {
105 		DotifyTranslatorImpl t = new DotifyTranslatorImpl(this, hyphenator);
106 		provider.rememberId(t);
107 		return t;
108 	}
109 	
110 	@Override
111 	public FromStyledTextToBraille fromStyledTextToBraille() {
112 		return fromStyledTextToBraille;
113 	}
114 	
115 	private final FromStyledTextToBraille fromStyledTextToBraille = new FromStyledTextToBraille() {
116 		public java.lang.Iterable<CSSStyledText> transform(java.lang.Iterable<CSSStyledText> styledText, int from, int to) {
117 			int size = size(styledText);
118 			if (to < 0) to = size;
119 			CSSStyledText[] braille = new CSSStyledText[to - from];
120 			int i = 0;
121 			for (CSSStyledText t : styledText) {
122 				if (i >= from && i < to) {
123 					SimpleInlineStyle style = t.getStyle();
124 					if (style != null)
125 						style = (SimpleInlineStyle)style.clone();
126 					String b = DotifyTranslatorImpl.this.transform(t.getText(), style);
127 					Map<String,String> a = t.getTextAttributes();
128 					if (a != null)
129 						a = new HashMap<>(a);
130 					braille[i - from] = new CSSStyledText(b, style, a);
131 				}
132 				i++; }
133 			return Arrays.asList(braille);
134 		}
135 	};
136 	
137 	@Override
138 	public LineBreakingFromStyledText lineBreakingFromStyledText() {
139 		return lineBreakingFromStyledText;
140 	}
141 	
142 	private final LineBreakingFromStyledText lineBreakingFromStyledText
143 	= new DefaultLineBreaker() {
144 		protected BrailleStream translateAndHyphenate(final java.lang.Iterable<CSSStyledText> styledText, int from, int to) {
145 			List<String> braille = new ArrayList<>();
146 			for (CSSStyledText t : fromStyledTextToBraille.transform(styledText, from, to)) {
147 				braille.add(t.getText());
148 				SimpleInlineStyle s = t.getStyle();
149 				if (s != null)
150 					for (String prop : s.getPropertyNames())
151 						logger.warn("{}: {} not supported", prop, s.get(prop));
152 			}
153 			return new FullyHyphenatedAndTranslatedString(join(braille));
154 		}
155 	};
156 	
157 	private final static SimpleInlineStyle HYPHENS_AUTO = TextStyleParser.getInstance().parse("hyphens: auto");
158 	
159 	private String transform(String text, boolean hyphenate) {
160 		if (hyphenate && !hyphenating)
161 			throw new RuntimeException("'hyphens: auto' is not supported");
162 		try {
163 			if (hyphenate && externalHyphenator != null) {
164 				String hyphenatedText = externalHyphenator.asFullHyphenator().transform(
165 					Collections.singleton(new CSSStyledText(text, HYPHENS_AUTO))).iterator().next().getText();
166 				return filter.filter(Translatable.text(hyphenatedText).hyphenate(false).build());
167 			} else
168 				return filter.filter(Translatable.text(text).hyphenate(hyphenate).build()); }
169 		catch (TranslationException e) {
170 			throw new RuntimeException(e); }
171 	}
172 	
173 	private String transform(String text, SimpleInlineStyle style) {
174 		boolean hyphenate = false;
175 		if (style != null) {
176 			CSSProperty val = style.getProperty("hyphens");
177 			if (val != null) {
178 				if (val == Hyphens.AUTO)
179 					hyphenate = true;
180 				else if (val == Hyphens.NONE)
181 					text = extractHyphens(text, false, SHY, ZWSP)._1;
182 				style.removeProperty("hyphens"); }}
183 		return transform(text, hyphenate);
184 	}
185 	
186 	@Component(
187 		name = "org.daisy.pipeline.braille.dotify.DotifyTranslatorImpl.Provider",
188 		service = {
189 			DotifyTranslator.Provider.class,
190 			BrailleTranslatorProvider.class,
191 			TransformProvider.class
192 		}
193 	)
194 	public static class Provider extends AbstractTransformProvider<DotifyTranslator>
195 	                             implements DotifyTranslator.Provider {
196 		
197 		public Iterable<DotifyTranslator> _get(Query query) {
198 			MutableQuery q = mutableQuery(query);
199 			for (Feature f : q.removeAll("input"))
200 				if (!supportedInput.contains(f.getValue().get()))
201 					return empty;
202 			for (Feature f : q.removeAll("output"))
203 				if (!supportedOutput.contains(f.getValue().get()))
204 					return empty;
205 			if (q.containsKey("translator"))
206 				if (!"dotify".equals(q.removeOnly("translator").getValue().get()))
207 					return empty;
208 			return logSelect(q, _provider);
209 		}
210 		
211 		protected DotifyTranslator rememberId(DotifyTranslator t) {
212 			return super.rememberId(t);
213 		}
214 		
215 		private final static Iterable<DotifyTranslator> empty = Iterables.<DotifyTranslator>empty();
216 		
217 		// "text-css" not supported: CSS styles not recognized and line breaking and white space
218 		// processing not according to CSS
219 		private final static List<String> supportedInput = Collections.emptyList();
220 		private final static List<String> supportedOutput = ImmutableList.of("braille");
221 		
222 		private TransformProvider<DotifyTranslator> _provider
223 		= varyLocale(
224 			new AbstractTransformProvider<DotifyTranslator>() {
225 				public Iterable<DotifyTranslator> _get(Query query) {
226 					MutableQuery q = mutableQuery(query);
227 					if (q.containsKey("language")
228 					    || q.containsKey("region")
229 					    || q.containsKey("locale")
230 					    || q.containsKey("document-locale")) {
231 						final Locale documentLocale;
232 						final Locale locale; {
233 							try {
234 								documentLocale = q.containsKey("document-locale")
235 									? parseLocale(q.removeOnly("document-locale").getValue().get())
236 									: null;
237 								if (q.containsKey("locale"))
238 									locale = parseLocale(q.removeOnly("locale").getValue().get());
239 								else {
240 									Locale language = q.containsKey("language")
241 										? parseLocale(q.removeOnly("language").getValue().get())
242 										: documentLocale;
243 									Locale region = q.containsKey("region")
244 										? parseLocale(q.removeOnly("region").getValue().get())
245 										: null;
246 									if (region != null) {
247 										// If region is specified we use it, but only if its language subtag matches the
248 										// specified language or the document locale (because Dotify only has a single
249 										// "locale" parameter).
250 										if (language != null)
251 											if (!language.equals(new Locale(language.getLanguage()))
252 											    || !region.getLanguage().equals(language.getLanguage()))
253 												return empty;
254 										locale = region;
255 									} else if (language != null)
256 										locale = language;
257 									else
258 										return empty;
259 								}
260 							} catch (IllegalArgumentException e) {
261 								logger.error("Invalid locale", e);
262 								return empty; }
263 						}
264 						final String mode = TranslatorMode.Builder.withType(TranslatorType.UNCONTRACTED).build().toString();
265 						if (!q.isEmpty()) {
266 							logger.warn("Unsupported feature '"+ q.iterator().next().getKey() + "'");
267 							return empty; }
268 						Iterable<BrailleFilter> filters = Iterables.transform(
269 							factoryServices,
270 							new Function<BrailleFilterFactoryService,BrailleFilter>() {
271 								public BrailleFilter _apply(BrailleFilterFactoryService service) {
272 									try {
273 										if (service.supportsSpecification(locale.toLanguageTag(), mode))
274 											return service.newFactory().newFilter(locale.toLanguageTag(), mode); }
275 									catch (TranslatorConfigurationException e) {
276 										logger.error("Could not create BrailleFilter for locale " + locale + " and mode " + mode, e); }
277 									throw new NoSuchElementException(); }});
278 						return concat(
279 							Iterables.transform(
280 								filters,
281 								new Function<BrailleFilter,Iterable<DotifyTranslator>>() {
282 									public Iterable<DotifyTranslator> _apply(final BrailleFilter filter) {
283 										Iterable<DotifyTranslator> translators = empty;
284 										if (documentLocale != null && locale.getLanguage().equals(documentLocale.getLanguage()))
285 											translators = concat(
286 												translators,
287 												Iterables.of(
288 													logCreate((DotifyTranslator)new DotifyTranslatorImpl(filter, true, Provider.this))));
289 										translators = concat(
290 											translators,
291 											Iterables.of(
292 												logCreate((DotifyTranslator)new DotifyTranslatorImpl(filter, false, Provider.this))));
293 										return translators;
294 									}
295 								}
296 							)
297 						);
298 					}
299 					return empty;
300 				}
301 			}
302 		);
303 		
304 		private final List<BrailleFilterFactoryService> factoryServices = new ArrayList<BrailleFilterFactoryService>();
305 		
306 		@Reference(
307 			name = "BrailleFilterFactoryService",
308 			unbind = "-",
309 			service = BrailleFilterFactoryService.class,
310 			cardinality = ReferenceCardinality.MULTIPLE,
311 			policy = ReferencePolicy.STATIC
312 		)
313 		protected void bindBrailleFilterFactoryService(BrailleFilterFactoryService service) {
314 			if (!OSGiHelper.inOSGiContext())
315 				service.setCreatedWithSPI();
316 			factoryServices.add(service);
317 			invalidateCache();
318 		}
319 		
320 		protected void unbindBrailleFilterFactoryService(BrailleFilterFactoryService service) {
321 			factoryServices.remove(service);
322 			invalidateCache();
323 		}
324 		
325 		@Override
326 		public ToStringHelper toStringHelper() {
327 			return MoreObjects.toStringHelper("DotifyTranslatorImpl$Provider");
328 		}
329 	}
330 	
331 	private static final Logger logger = LoggerFactory.getLogger(DotifyTranslatorImpl.class);
332 	
333 	private static abstract class OSGiHelper {
334 		static boolean inOSGiContext() {
335 			try {
336 				return FrameworkUtil.getBundle(OSGiHelper.class) != null;
337 			} catch (NoClassDefFoundError e) {
338 				return false;
339 			}
340 		}
341 	}
342 }