1   package org.daisy.pipeline.braille.css.calabash.impl;
2   
3   import java.util.ArrayList;
4   import java.util.List;
5   import java.util.Map;
6   import java.util.NoSuchElementException;
7   import java.util.Set;
8   import java.util.Stack;
9   import java.util.TreeMap;
10  import java.util.TreeSet;
11  
12  import javax.xml.namespace.QName;
13  import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
14  import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;
15  import javax.xml.stream.XMLStreamException;
16  import javax.xml.stream.XMLStreamWriter;
17  
18  import com.xmlcalabash.core.XProcRuntime;
19  import com.xmlcalabash.io.ReadablePipe;
20  import com.xmlcalabash.io.WritablePipe;
21  import com.xmlcalabash.library.DefaultStep;
22  import com.xmlcalabash.runtime.XAtomicStep;
23  
24  import net.sf.saxon.s9api.SaxonApiException;
25  
26  import org.daisy.common.saxon.SaxonBuffer;
27  import org.daisy.common.stax.BaseURIAwareXMLStreamReader;
28  import org.daisy.common.stax.BaseURIAwareXMLStreamWriter;
29  import org.daisy.common.stax.XMLStreamWriterHelper.BufferedXMLStreamWriter;
30  import org.daisy.common.stax.XMLStreamWriterHelper.FutureWriterEvent;
31  import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttribute;
32  import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttributes;
33  import static org.daisy.common.stax.XMLStreamWriterHelper.writeEvent;
34  import static org.daisy.common.stax.XMLStreamWriterHelper.writeStartElement;
35  import org.daisy.common.transform.InputValue;
36  import org.daisy.common.transform.SingleInSingleOutXMLTransformer;
37  import org.daisy.common.transform.TransformerException;
38  import org.daisy.common.transform.XMLInputValue;
39  import org.daisy.common.transform.XMLOutputValue;
40  import org.daisy.common.xproc.calabash.XMLCalabashInputValue;
41  import org.daisy.common.xproc.calabash.XMLCalabashOutputValue;
42  import org.daisy.common.xproc.calabash.XProcStep;
43  import org.daisy.common.xproc.calabash.XProcStepProvider;
44  import org.daisy.common.xproc.XProcMonitor;
45  
46  import org.osgi.service.component.annotations.Component;
47  
48  import org.slf4j.Logger;
49  import org.slf4j.LoggerFactory;
50  
51  public class CssShiftIdStep extends DefaultStep implements XProcStep {
52  	
53  	@Component(
54  		name = "css:shift-id",
55  		service = { XProcStepProvider.class },
56  		property = { "type:String={http://www.daisy.org/ns/pipeline/braille-css}shift-id" }
57  	)
58  	public static class Provider implements XProcStepProvider {
59  		
60  		@Override
61  		public XProcStep newStep(XProcRuntime runtime, XAtomicStep step, XProcMonitor monitor, Map<String,String> properties) {
62  			return new CssShiftIdStep(runtime, step);
63  		}
64  	}
65  	
66  	private ReadablePipe sourcePipe = null;
67  	private WritablePipe resultPipe = null;
68  	
69  	private static final String XMLNS_CSS = "http://www.daisy.org/ns/pipeline/braille-css";
70  	private static final QName CSS_ID = new QName(XMLNS_CSS, "id");
71  	private static final QName CSS_BOX = new QName(XMLNS_CSS, "box");
72  	private static final QName CSS__ = new QName(XMLNS_CSS, "_");
73  	private static final QName _TYPE = new QName("type");
74  	private static final QName CSS_COUNTER = new QName(XMLNS_CSS, "counter");
75  	private static final QName _NAME = new QName("name");
76  	private static final QName _TARGET = new QName("target");
77  	private static final QName CSS_ANCHOR = new QName(XMLNS_CSS, "anchor");
78  	private static final QName CSS_FLOW = new QName(XMLNS_CSS, "flow");
79  	
80  	private CssShiftIdStep(XProcRuntime runtime, XAtomicStep step) {
81  		super(runtime, step);
82  	}
83  	
84  	@Override
85  	public void setInput(String port, ReadablePipe pipe) {
86  		sourcePipe = pipe;
87  	}
88  	
89  	@Override
90  	public void setOutput(String port, WritablePipe pipe) {
91  		resultPipe = pipe;
92  	}
93  	
94  	@Override
95  	public void reset() {
96  		sourcePipe.resetReader();
97  		resultPipe.resetWriter();
98  	}
99  	
100 	@Override
101 	public void run() throws SaxonApiException {
102 		super.run();
103 		try {
104 			// Two passes: one for shifting id, second for updating references.
105 			// Alternative is to combine the two and make use of FutureEvent for forward references.
106 			Map<String,String> idMap = new TreeMap<String,String>();
107 			Set<String> discardedIds = new TreeSet<String>();
108 			new ShiftIdTransformer(idMap, discardedIds)
109 			.andThen(new UpdateRefsTransformer(idMap, discardedIds),
110 			         new SaxonBuffer(runtime.getProcessor().getUnderlyingConfiguration()),
111 			         false)
112 			.transform(
113 				new XMLCalabashInputValue(sourcePipe),
114 				new XMLCalabashOutputValue(resultPipe, runtime))
115 			.run(); }
116 		catch (Throwable e) {
117 			throw XProcStep.raiseError(e, step); }
118 	}
119 	
120 	private static class ShiftIdTransformer extends SingleInSingleOutXMLTransformer {
121 		
122 		private final Map<String,String> idMap;
123 		private final Set<String> discardedIds;
124 		
125 		public ShiftIdTransformer(Map<String,String> idMap, Set<String> discardedIds) {
126 			this.idMap = idMap;
127 			this.discardedIds = discardedIds;
128 		}
129 		
130 		public Runnable transform(XMLInputValue<?> source, XMLOutputValue<?> result, InputValue<?> params) throws IllegalArgumentException {
131 			if (source == null || result == null)
132 				throw new IllegalArgumentException();
133 			return () -> transform(source.asXMLStreamReader(), result.asXMLStreamWriter());
134 		}
135 
136 		public void transform(BaseURIAwareXMLStreamReader reader, BaseURIAwareXMLStreamWriter output) throws TransformerException {
137 			BufferedXMLStreamWriter writer = new BufferedXMLStreamWriter(output);
138 			boolean insideInlineBox = false;
139 			Stack<Boolean> blockBoxes = new Stack<Boolean>();
140 			Stack<Boolean> inlineBoxes = new Stack<Boolean>();
141 			List<String> pendingId = new ArrayList<String>();
142 			ShiftedId shiftedId = null;
143 			int depth = 0;
144 			String currentFlow = "normal";
145 			try {
146 				int event = reader.getEventType();
147 				while (true)
148 					try {
149 						switch (event) {
150 						case START_ELEMENT: {
151 							writeEvent(writer, reader);
152 							boolean isInlineBox = false;
153 							boolean isBlockBox = false;
154 							if (insideInlineBox)
155 								writeAttributes(writer, reader);
156 							else {
157 								boolean isBox = CSS_BOX.equals(reader.getName());
158 								String id = null;
159 								String flow = depth == 0 ? "normal" : currentFlow;
160 								for (int i = 0; i < reader.getAttributeCount(); i++) {
161 									QName name = reader.getAttributeName(i);
162 									String value = reader.getAttributeValue(i);
163 									if (CSS_ID.equals(name))
164 										id = value;
165 									else {
166 										if (isBox && _TYPE.equals(name))
167 											if ("inline".equalsIgnoreCase(value))
168 												isInlineBox = true;
169 											else if ("block".equalsIgnoreCase(value))
170 												isBlockBox = true;
171 										if (depth == 0 && CSS_FLOW.equals(name))
172 											flow = value;
173 										writeAttribute(writer, name, value); }}
174 								boolean newFlow = !flow.equals(currentFlow);
175 								currentFlow = flow;
176 								if (newFlow || isBlockBox) {
177 									if (!pendingId.isEmpty()) {
178 										if (shiftedId == null) {
179 											if (newFlow) {
180 												discardedIds.addAll(pendingId);
181 												pendingId.clear(); }}
182 										else {
183 											shiftedId.putAll(pendingId);
184 											pendingId.clear(); }}
185 									if (shiftedId != null) {
186 										shiftedId.render();
187 										shiftedId = null; }}
188 								if (isInlineBox && shiftedId != null) {
189 									shiftedId.render(); // will be empty
190 									shiftedId = null; }
191 								if (isInlineBox) {
192 									if (id != null)
193 										if (!pendingId.isEmpty())
194 											pendingId.add(id);
195 										else
196 											writeAttribute(writer, CSS_ID, id);
197 									if (!pendingId.isEmpty()) {
198 										id = pendingId.get(pendingId.size() - 1);
199 										for (String oldId : pendingId)
200 											if (!oldId.equals(id))
201 												idMap.put(oldId, id);
202 										pendingId.clear();
203 										if (id != null)
204 											writeAttribute(writer, CSS_ID, id); }}
205 								else if (id != null)
206 									pendingId.add(id);
207 								if (isInlineBox)
208 									insideInlineBox = true; }
209 							blockBoxes.push(isBlockBox);
210 							inlineBoxes.push(isInlineBox);
211 							depth++;
212 							break; }
213 						case END_ELEMENT: {
214 							depth--;
215 							boolean isBlockBox = blockBoxes.pop();
216 							boolean isInlineBox = inlineBoxes.pop();
217 							if (isBlockBox) {
218 								if (!pendingId.isEmpty()) {
219 									if (shiftedId == null)
220 										discardedIds.addAll(pendingId);
221 									else
222 										shiftedId.putAll(pendingId);
223 									pendingId.clear(); }}
224 							else if (isInlineBox) {
225 								if (shiftedId != null)
226 									throw new RuntimeException("coding error");
227 								shiftedId = new ShiftedId();
228 								writer.writeEvent(shiftedId);
229 								insideInlineBox = false; }
230 							writeEvent(writer, reader);
231 							break; }
232 						default:
233 							writeEvent(writer, reader); }
234 						event = reader.next(); }
235 					catch (NoSuchElementException e) {
236 						break; }
237 				if (!pendingId.isEmpty())
238 					if (shiftedId == null)
239 						throw new RuntimeException("invalid input");
240 					else
241 						shiftedId.putAll(pendingId);
242 				if (shiftedId != null)
243 					shiftedId.render();
244 				writer.flush(); }
245 			catch (XMLStreamException e) {
246 				throw new TransformerException(e); }
247 		}
248 		
249 		private class ShiftedId implements FutureWriterEvent {
250 			
251 			private List<String> ids;
252 			private boolean ready = false;
253 			
254 			private void put(String id) {
255 				if (this.ids == null)
256 					this.ids = new ArrayList<String>();
257 				this.ids.add(id);
258 			}
259 			
260 			private void putAll(List<String> ids) {
261 				for (String id : ids) put(id);
262 			}
263 			
264 			private void render() {
265 				ready = true;
266 			}
267 			
268 			public void writeTo(XMLStreamWriter writer) throws XMLStreamException {
269 				if (!ready)
270 					throw new XMLStreamException("not ready");
271 				if (ids != null) {
272 					String id = ids.get(0);
273 					for (String oldId : ids)
274 						if (!oldId.equals(id))
275 							idMap.put(oldId, id);
276 					writeStartElement(writer, CSS__);
277 					writeAttribute(writer, CSS_ID, id);
278 					writer.writeEndElement(); }
279 			}
280 			
281 			public boolean isReady() {
282 				return ready;
283 			}
284 		}
285 	}
286 	
287 	private static class UpdateRefsTransformer extends SingleInSingleOutXMLTransformer {
288 		
289 		private final Map<String,String> idMap;
290 		private final Set<String> discardedIds;
291 		
292 		public UpdateRefsTransformer(Map<String,String> idMap, Set<String> discardedIds) {
293 			this.idMap = idMap;
294 			this.discardedIds = discardedIds;
295 		}
296 		
297 		public Runnable transform(XMLInputValue<?> source, XMLOutputValue<?> result, InputValue<?> params) throws IllegalArgumentException {
298 			if (source == null || result == null)
299 				throw new IllegalArgumentException();
300 			return () -> transform(source.asXMLStreamReader(), result.asXMLStreamWriter());
301 		}
302 		
303 		void transform(BaseURIAwareXMLStreamReader reader, BaseURIAwareXMLStreamWriter output) throws TransformerException {
304 			BufferedXMLStreamWriter writer = new BufferedXMLStreamWriter(output);
305 			try {
306 				while (true)
307 					try {
308 						int event = reader.getEventType();
309 						switch (event) {
310 						case START_ELEMENT: {
311 							writeEvent(writer, reader);
312 							QName elemName = reader.getName();
313 							boolean isCounter = CSS_COUNTER.equals(elemName);
314 							String counterTarget = null;
315 							String anchor = null;
316 							for (int i = 0; i < reader.getAttributeCount(); i++) {
317 								QName attrName = reader.getAttributeName(i);
318 								String attrValue = reader.getAttributeValue(i);
319 								if (CSS_ANCHOR.equals(attrName))
320 									anchor = attrValue;
321 								else if (isCounter && _TARGET.equals(attrName)) {
322 									counterTarget = attrValue;
323 								} else {
324 									writeAttribute(writer, attrName, attrValue);
325 								}
326 							}
327 							if (anchor != null) {
328 								if (idMap.containsKey(anchor)) {
329 									anchor = idMap.get(anchor);
330 								} else if (discardedIds.contains(anchor)) {
331 									throw new RuntimeException();
332 								}
333 								writeAttribute(writer, CSS_ANCHOR, anchor);
334 							}
335 							if (counterTarget != null) {
336 								if (idMap.containsKey(counterTarget)) {
337 									counterTarget = idMap.get(counterTarget);
338 								} else if (discardedIds.contains(counterTarget)) {
339 									throw new RuntimeException();
340 								}
341 								writeAttribute(writer, _TARGET, counterTarget);
342 							}
343 							break; }
344 						default:
345 							writeEvent(writer, reader); }
346 						reader.next(); }
347 					catch (NoSuchElementException e) {
348 						break; }
349 				writer.flush(); }
350 			catch (XMLStreamException e) {
351 				throw new TransformerException(e); }
352 		}
353 	}
354 	
355 	private static final Logger logger = LoggerFactory.getLogger(CssShiftIdStep.class);
356 	
357 }