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