1   package org.daisy.pipeline.braille.common;
2   
3   import java.net.URI;
4   import java.util.ArrayList;
5   import java.util.Collections;
6   import java.util.function.Supplier;
7   import java.util.List;
8   import java.util.Map;
9   
10  import com.google.common.base.Predicates;
11  import com.google.common.collect.Iterables;
12  import com.google.common.collect.Maps;
13  
14  import org.daisy.pipeline.braille.common.Query.MutableQuery;
15  import static org.daisy.pipeline.braille.common.Query.util.mutableQuery;
16  import static org.daisy.pipeline.braille.common.TransformProvider.util.dispatch;
17  import static org.daisy.pipeline.braille.common.TransformProvider.util.logCreate;
18  import org.daisy.pipeline.braille.common.TransformProvider.util.Memoize;
19  import static org.daisy.pipeline.braille.common.util.Locales.parseLocale;
20  import org.daisy.pipeline.braille.common.util.Strings;
21  
22  import org.osgi.service.component.annotations.Component;
23  import org.osgi.service.component.annotations.Reference;
24  import org.osgi.service.component.annotations.ReferenceCardinality;
25  import org.osgi.service.component.annotations.ReferencePolicy;
26  
27  import org.slf4j.Logger;
28  import org.slf4j.LoggerFactory;
29  
30  @Component(
31  	name = "braille-translator-registry",
32  	service = { BrailleTranslatorRegistry.class }
33  )
34  public class BrailleTranslatorRegistry extends Memoize<BrailleTranslator>
35                                         implements BrailleTranslatorProvider<BrailleTranslator> {
36  
37  	public BrailleTranslatorRegistry() {
38  		super(true); // memoize (id:...) lookups only
39  		providers = new ArrayList<>();
40  		dispatch = dispatch(providers);
41  		context = logger;
42  		unmodifiable = false;
43  	}
44  
45  	private BrailleTranslatorRegistry(TransformProvider<BrailleTranslator> dispatch,
46  	                                  HyphenatorRegistry hyphenatorRegistry,
47  	                                  BrailleTranslatorRegistry from,
48  	                                  Logger context) {
49  		super(from);
50  		this.providers = null;
51  		this.dispatch = dispatch;
52  		this.hyphenatorRegistry = hyphenatorRegistry;
53  		this.context = context;
54  		this.unmodifiable = true;
55  	}
56  
57  	private final List<TransformProvider<BrailleTranslator>> providers;
58  	private final TransformProvider<BrailleTranslator> dispatch;
59  	private final Logger context;
60  	private final boolean unmodifiable;
61  
62  	@Reference(
63  		name = "BrailleTranslatorProvider",
64  		unbind = "-",
65  		service = BrailleTranslatorProvider.class,
66  		cardinality = ReferenceCardinality.MULTIPLE,
67  		policy = ReferencePolicy.STATIC
68  	)
69  	/**
70  	 * @throws UnsupportedOperationException if this object is an unmodifiable view of another
71  	 * {@link BrailleTranslatorRegistry}.
72  	 */
73  	public void addProvider(BrailleTranslatorProvider p) throws UnsupportedOperationException {
74  		if (unmodifiable)
75  			throw new UnsupportedOperationException("Unmodifiable");
76  		providers.add(p);
77  	}
78  
79  	@Override
80  	public Iterable<BrailleTranslator> _get(Query q) {
81  		return dispatch.get(q);
82  	}
83  
84  	@Override
85  	public BrailleTranslatorRegistry withContext(Logger context) {
86  		return (BrailleTranslatorRegistry)super.withContext(context);
87  	}
88  	
89  	protected BrailleTranslatorRegistry _withContext(Logger context) {
90  		if (this.context == context)
91  			return this;
92  		return new BrailleTranslatorRegistry(dispatch.withContext(context),
93  		                                     hyphenatorRegistry.withContext(context),
94  		                                     this,
95  		                                     context);
96  	}
97  
98  	private HyphenatorRegistry hyphenatorRegistry;
99  
100 	@Reference(
101 		name = "HyphenatorRegistry",
102 		unbind = "-",
103 		service = HyphenatorRegistry.class,
104 		cardinality = ReferenceCardinality.MANDATORY,
105 		policy = ReferencePolicy.STATIC
106 	)
107 	/**
108 	 * @throws UnsupportedOperationException if this object is an unmodifiable view of another
109 	 * {@link BrailleTranslatorRegistry}.
110 	 */
111 	public void bindHyphenatorRegistry(HyphenatorRegistry registry) throws UnsupportedOperationException {
112 		if (unmodifiable)
113 			throw new UnsupportedOperationException("Unmodifiable");
114 		hyphenatorRegistry = registry;
115 		logger.debug("Binding hyphenator registry: " + registry);
116 	}
117 
118 	/**
119 	 * Select {@link BrailleTranslator}s with {@link Hyphenator} based on a single query.
120 	 *
121 	 * Only the {@code hyphenator} and {@code document-locale} features contribute to the hyphenator
122 	 * selection.
123 	 *
124 	 * Like {@code #get(Query)}, returned objects are selectable based on their identifier.
125 	 */
126 	public Iterable<BrailleTranslator> getWithHyphenator(Query query) {
127 		return getWithHyphenator(this, hyphenatorRegistry, query, context);
128 	}
129 
130 	private static Iterable<BrailleTranslator> getWithHyphenator(Provider<Query,BrailleTranslator> translatorProvider,
131 	                                                             Provider<Query,Hyphenator> hyphenatorProvider,
132 	                                                             Query query,
133 	                                                             Logger context) {
134 		if (query.containsKey("id"))
135 			return translatorProvider.get(query);
136 		MutableQuery q = mutableQuery(query);
137 
138 		// select hyphenator
139 		MutableQuery hyphenatorQuery = mutableQuery();
140 		String hyphenator = q.containsKey("hyphenator")
141 			? q.removeOnly("hyphenator").getValue().get()
142 			: "auto";
143 		if (!"auto".equals(hyphenator))
144 			hyphenatorQuery.add("hyphenator", hyphenator);
145 		String documentLocale; {
146 			try {
147 				documentLocale = q.containsKey("document-locale")
148 					? parseLocale(q.getOnly("document-locale").getValue().get()).toLanguageTag()
149 					: null; }
150 			catch (IllegalArgumentException e) {
151 				logger.error("Invalid locale", e);
152 				return Collections.emptyList(); }
153 		}
154 		if (documentLocale != null)
155 			hyphenatorQuery.add("document-locale", documentLocale);
156 		Iterable<Hyphenator> hyphenators; {
157 			Iterable<Hyphenator> h = hyphenatorProvider.get(hyphenatorQuery.asImmutable());
158 			if (documentLocale != null && !"auto".equals(hyphenator)) {
159 				// also search without locale because "hyphenator" feature might be an ID
160 				hyphenatorQuery.removeAll("document-locale");
161 				h = Iterables.concat(h, hyphenatorProvider.get(hyphenatorQuery.asImmutable())); }
162 			hyphenators = h;
163 		}
164 
165 		// select translator and bind hyphenator
166 		Iterable<BrailleTranslator> translators = translatorProvider.get(q);
167 		Iterable<BrailleTranslator> translatorsWithHyphenator =
168 			Iterables.concat(
169 				Iterables.transform(
170 					translators,
171 					t -> Iterables.filter(
172 						Iterables.transform(
173 							hyphenators,
174 							h -> {
175 								try {
176 									return logCreate(t.withHyphenator(h), context);
177 								} catch (UnsupportedOperationException e) {
178 									logger.debug("Could not set hyphenator: " + e.getMessage());
179 									return null;
180 								}}),
181 						Predicates.notNull())));
182 		return "auto".equals(hyphenator)
183 			? Iterables.concat(translatorsWithHyphenator, translators)
184 			: translatorsWithHyphenator;
185 	}
186 
187 	/**
188 	 * Select {@link BrailleTranslator}s based on a query and a CSS style sheet possibly containing
189 	 * {@code @text-transform} rules.
190 	 *
191 	 * Contrary to {@link #get(Query)}, this method is not memoized, and the returned objects may
192 	 * not be selectable based on their identifier.
193 	 *
194 	 * @param baseURI Base URI for resolving relative paths in CSS against.
195 	 * @param forceMainTranslator if {@code true}, the translator defined in {@code query} is used
196 	 *                            even if a default translator has been defined in CSS.
197 	 */
198 	public Iterable<BrailleTranslator> get(Query query, String style, URI baseURI, boolean forceMainTranslator) {
199 		if (style != null) {
200 			Map<String,Query> subQueries = TextTransformParser.getBrailleTranslatorQueries(style, baseURI, query);
201 			if (subQueries != null && !subQueries.isEmpty()) {
202 				Query mainQuery = subQueries.remove("auto");
203 				if (mainQuery == null || forceMainTranslator)
204 					mainQuery = query;
205 				if (subQueries.isEmpty())
206 					return get(mainQuery);
207 				else {
208 					Map<String,Supplier<BrailleTranslator>> subTranslators
209 						= Maps.transformValues(
210 							subQueries,
211 							// using getWithHyphenator() because @text-transform rules may contain "hyphenator"
212 							// descriptor (though it should be deprecated)
213 							q -> () -> BrailleTranslatorRegistry.this.getWithHyphenator(q).iterator().next());
214 					return Iterables.transform(
215 						get(mainQuery),
216 						t -> logCreate(new CompoundBrailleTranslator(t, subTranslators), context));
217 				}
218 			}
219 		}
220 		return get(query);
221 	}
222 
223 	/**
224 	 * Select {@link BrailleTranslator}s with {@link Hyphenator} based on a single query and a CSS
225 	 * style sheet possibly containing {@code @text-transform} and {@code @hyphenation-resource}
226 	 * rules.
227 	 *
228 	 * Contrary to {@link #get(Query)} and similar to {@link #get(Query, String, URI, boolean)},
229 	 * this method is not memoized, and the returned objects may not be selectable based on their
230 	 * identifier.
231 	 */
232 	public Iterable<BrailleTranslator> getWithHyphenator(Query query, String style, URI baseURI, boolean forceMainTranslator) {
233 		return getWithHyphenator(
234 			q -> get(q, style, baseURI, forceMainTranslator),
235 			q -> hyphenatorRegistry.get(q, style, baseURI),
236 			query,
237 			context);
238 	}
239 
240 	@Override
241 	public String toString() {
242 		return "memoize(dispatch( " + Strings.join(providers, ", ") + " ))";
243 	}
244 
245 	private static final Logger logger = LoggerFactory.getLogger(BrailleTranslatorRegistry.class);
246 }