1   package org.daisy.pipeline.braille.common.impl;
2   
3   import java.net.URI;
4   import java.util.HashMap;
5   import java.util.Map;
6   
7   import javax.xml.namespace.QName;
8   
9   import com.google.common.base.MoreObjects;
10  import com.google.common.base.MoreObjects.ToStringHelper;
11  import com.google.common.collect.ImmutableMap;
12  
13  import com.xmlcalabash.core.XProcRuntime;
14  import com.xmlcalabash.runtime.XAtomicStep;
15  
16  import org.daisy.common.file.URLs;
17  import org.daisy.common.saxon.SaxonInputValue;
18  import org.daisy.common.transform.InputValue;
19  import org.daisy.common.transform.Mult;
20  import org.daisy.common.transform.SingleInSingleOutXMLTransformer;
21  import org.daisy.common.transform.TransformerException;
22  import org.daisy.common.transform.XMLInputValue;
23  import org.daisy.common.transform.XMLOutputValue;
24  import org.daisy.common.xproc.calabash.XProcBasedTransformer;
25  import org.daisy.common.xproc.calabash.XProcStep;
26  import org.daisy.common.xproc.calabash.XProcStepProvider;
27  import org.daisy.common.xproc.XProcMonitor;
28  import org.daisy.pipeline.braille.common.AbstractBrailleTranslator;
29  import org.daisy.pipeline.braille.common.AbstractTransformProvider;
30  import org.daisy.pipeline.braille.common.AbstractTransformProvider.util.Function;
31  import org.daisy.pipeline.braille.common.AbstractTransformProvider.util.Iterables;
32  import static org.daisy.pipeline.braille.common.AbstractTransformProvider.util.Iterables.transform;
33  import static org.daisy.pipeline.braille.common.AbstractTransformProvider.util.logCreate;
34  import static org.daisy.pipeline.braille.common.AbstractTransformProvider.util.logSelect;
35  import org.daisy.pipeline.braille.common.BrailleTranslator;
36  import org.daisy.pipeline.braille.common.BrailleTranslatorProvider;
37  import org.daisy.pipeline.braille.common.BrailleTranslatorRegistry;
38  import org.daisy.pipeline.braille.common.Hyphenator;
39  import org.daisy.pipeline.braille.common.HyphenatorRegistry;
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 org.daisy.pipeline.braille.common.util.Function0;
46  import org.daisy.pipeline.braille.common.util.Functions;
47  
48  import org.osgi.service.component.annotations.Activate;
49  import org.osgi.service.component.annotations.Component;
50  import org.osgi.service.component.annotations.Reference;
51  import org.osgi.service.component.annotations.ReferenceCardinality;
52  import org.osgi.service.component.annotations.ReferencePolicy;
53  
54  import org.slf4j.Logger;
55  import org.slf4j.LoggerFactory;
56  
57  import org.w3c.dom.Document;
58  import org.w3c.dom.Node;
59  
60  /**
61   * @see <a href="../../../../../../../README.md">Documentation</a>
62   * @see <a href="../../../../../../../resources/xml/block-translator.xpl">XProc code</a>
63   */
64  public interface CSSBlockTransform {
65  	
66  	@Component(
67  		name = "org.daisy.pipeline.braille.common.impl.CSSBlockTransform.Provider",
68  		service = {
69  			BrailleTranslatorProvider.class,
70  			TransformProvider.class
71  		}
72  	)
73  	public class Provider extends AbstractTransformProvider<BrailleTranslator> implements BrailleTranslatorProvider<BrailleTranslator> {
74  		
75  		private URI href;
76  		
77  		@Activate
78  		protected void activate(final Map<?,?> properties) {
79  			href = URLs.asURI(URLs.getResourceFromJAR("xml/block-translator.xpl", CSSBlockTransform.class));
80  			translatorRegistry = translatorRegistry.withContext(logger);
81  		}
82  		
83  		private final static Iterable<BrailleTranslator> empty = Iterables.<BrailleTranslator>empty();
84  		
85  		@Override
86  		protected Iterable<BrailleTranslator> _get(Query query) {
87  			final MutableQuery q = mutableQuery(query);
88  			for (Feature f : q.removeAll("input"))
89  				if ("html".equals(f.getValue().get())) {}
90  				else if (!"css".equals(f.getValue().get()))
91  					return empty;
92  			boolean braille = false; {
93  				for (Feature f : q.removeAll("output"))
94  					if ("css".equals(f.getValue().get())) {}
95  					else if ("html".equals(f.getValue().get())) {}
96  					else if ("braille".equals(f.getValue().get()))
97  						braille = true;
98  					else
99  						return empty;
100 			}
101 			final String brailleCharset = q.containsKey("braille-charset")
102 				? q.getOnly("braille-charset").getValue().get()
103 				: null;
104 			final boolean includeBrailleCodeInLanguage = q.containsKey("include-braille-code-in-language")
105 				? q.getOnly("include-braille-code-in-language").getValue().orElse("true").equalsIgnoreCase("true")
106 				: false;
107 			q.add("input", "text-css");
108 			if (braille)
109 				q.add("output", "braille");
110 			return transform(
111 				logSelect(q, translatorRegistry.getWithHyphenator(q)),
112 				new Function<BrailleTranslator,BrailleTranslator>() {
113 					public BrailleTranslator _apply(BrailleTranslator translator) {
114 						return __apply(
115 							logCreate(new TransformImpl(translator, false, brailleCharset,
116 							                            includeBrailleCodeInLanguage, q))
117 						);
118 					}
119 				}
120 			);
121 		}
122 			
123 		@Override
124 		public ToStringHelper toStringHelper() {
125 			return MoreObjects.toStringHelper("CSSBlockTransform$Provider");
126 		}
127 		
128 		private class TransformImpl extends AbstractBrailleTranslator implements XProcStepProvider {
129 			
130 			private final Query mainQuery;
131 			private final BrailleTranslator mainTranslator;
132 			private final boolean forceMainTranslator;
133 			private final Map<String,String> options;
134 			private final String brailleCharset;
135 			
136 			/**
137 			 * @param mainTranslator translator to be used for the parts of the document that do
138 			 *                       not have their own sub-translator
139 			 * @param forceMainTranslator if true, the main translator is used even if a default
140 			 *                            translator has been defined in CSS
141 			 */
142 			// FIXME: mainTranslator is optional if default translator has been defined in CSS (which we can not know in advance)
143 			private TransformImpl(BrailleTranslator mainTranslator, boolean forceMainTranslator,
144 			                      String brailleCharset, boolean includeBrailleCodeInLanguage, Query query) {
145 				options = ImmutableMap.of("braille-charset", brailleCharset != null ? brailleCharset : "",
146 				                          "include-braille-code-in-language", "" + includeBrailleCodeInLanguage);
147 				this.mainTranslator = mainTranslator;
148 				this.forceMainTranslator = forceMainTranslator;
149 				mainQuery = query;
150 				this.brailleCharset = brailleCharset;
151 			}
152 			
153 			private Hyphenator hyphenator = null;
154 			
155 			private TransformImpl(TransformImpl from, BrailleTranslator mainTranslator) {
156 				super(from);
157 				this.mainQuery = from.mainQuery;
158 				this.mainTranslator = mainTranslator;
159 				this.forceMainTranslator = from.forceMainTranslator;
160 				this.options = from.options;
161 				this.brailleCharset = from.brailleCharset;
162 			}
163 			
164 			/**
165 			 * @throws UnsupportedOperationException if {@code mainTranslator.withHyphenator()} throws
166 			 *                                       UnsupportedOperationException
167 			 */
168 			@Override
169 			public TransformImpl _withHyphenator(Hyphenator hyphenator) throws UnsupportedOperationException {
170 				TransformImpl t = new TransformImpl(this, mainTranslator.withHyphenator(hyphenator));
171 				Provider.this.rememberId(t);
172 				return t;
173 			}
174 			
175 			@Override
176 			public XProcStep newStep(XProcRuntime runtime, XAtomicStep step, XProcMonitor monitor, Map<String,String> properties) {
177 				return XProcStep.of(
178 					new SingleInSingleOutXMLTransformer() {
179 						public Runnable transform(XMLInputValue<?> source, XMLOutputValue<?> result, InputValue<?> params) {
180 							return () -> {
181 								if (!(source instanceof SaxonInputValue))
182 									throw new IllegalArgumentException();
183 								Mult<SaxonInputValue> mult = ((SaxonInputValue)source).mult(2);
184 
185 								// analyze the input
186 								Node doc = mult.get().ensureSingleItem().asNodeIterator().next();
187 								if (!(doc instanceof Document))
188 									throw new TransformerException(new IllegalArgumentException());
189 								String style = (((Document)doc).getDocumentElement()).getAttribute("style");
190 								URI styleBaseURI = URLs.asURI(((Document)doc).getBaseURI());
191 								BrailleTranslator compoundTranslator
192 									= translatorRegistry.getWithHyphenator(mainQuery, style, styleBaseURI, forceMainTranslator)
193 									                    .iterator().next();
194 								if (hyphenator != null)
195 									compoundTranslator = compoundTranslator.withHyphenator(hyphenator);
196 								Function0<Void> evictTempTranslator; {
197 									if (compoundTranslator != mainTranslator)
198 										// translatorRegistry.get() call above probably returned an object that was not cached
199 										evictTempTranslator = Provider.this.provideTemporarily(compoundTranslator);
200 									else
201 										evictTempTranslator = Functions.noOp;
202 								}
203 
204 								// run the transformation
205 								Map<QName,InputValue<?>> options = new HashMap<>();
206 								for (String option : TransformImpl.this.options.keySet())
207 									options.put(new QName(option), new InputValue<>(TransformImpl.this.options.get(option)));
208 								options.put(new QName("text-transform"),
209 								            new InputValue<>(
210 									            mutableQuery().add("id", compoundTranslator.getIdentifier()).toString()));
211 								new XProcBasedTransformer(
212 									href,
213 									options
214 								).newStep(runtime, step, monitor, properties).transform(
215 									ImmutableMap.of(
216 										new QName("source"), mult.get(),
217 										new QName("parameters"), params),
218 									ImmutableMap.of(
219 										new QName("result"), result)
220 								).run();
221 								evictTempTranslator.apply();
222 							};
223 						}
224 					},
225 					runtime,
226 					step
227 				);
228 			}
229 			
230 			@Override
231 			public ToStringHelper toStringHelper() {
232 				return MoreObjects.toStringHelper("CSSBlockTransform$Provider$TransformImpl")
233 					.add("translator", mainTranslator);
234 			}
235 		}
236 		
237 		// FIXME: should ideally bind BrailleTranslatorRegistry, but that does not work because
238 		// BrailleTranslatorRegistry binds BrailleTranslatorProvider, which CSSBlockTransform
239 		// implements, so there would be a cyclic dependency. Note that this is very brittle,
240 		// because there will be a cyclic dependency anyway if at least one other
241 		// BrailleTranslatorProvider would bind BrailleTranslatorProvider.
242 
243 		@Reference(
244 			name = "BrailleTranslatorProvider",
245 			unbind = "-",
246 			service = BrailleTranslatorProvider.class,
247 			cardinality = ReferenceCardinality.MULTIPLE,
248 			policy = ReferencePolicy.STATIC
249 		)
250 		protected void bindBrailleTranslatorProvider(BrailleTranslatorProvider<?> provider) {
251 			translatorRegistry.addProvider(provider);
252 		}
253 		
254 		@Reference(
255 			name = "HyphenatorRegistry",
256 			unbind = "-",
257 			service = HyphenatorRegistry.class,
258 			cardinality = ReferenceCardinality.MANDATORY,
259 			policy = ReferencePolicy.STATIC
260 		)
261 		protected void bindHyphenatorRegistry(HyphenatorRegistry registry) {
262 			translatorRegistry.bindHyphenatorRegistry(registry);
263 		}
264 		
265 		private BrailleTranslatorRegistry translatorRegistry = new BrailleTranslatorRegistry();
266 		
267 		private static final Logger logger = LoggerFactory.getLogger(Provider.class);
268 		
269 	}
270 }