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