1   package org.daisy.pipeline.nlp.calabash.impl;
2   
3   import java.util.HashSet;
4   import java.util.List;
5   import java.util.Locale;
6   import java.util.Map;
7   import java.util.Set;
8   
9   import net.sf.saxon.om.NodeInfo;
10  import net.sf.saxon.s9api.Axis;
11  import net.sf.saxon.s9api.QName;
12  import net.sf.saxon.s9api.XdmNode;
13  import net.sf.saxon.s9api.XdmNodeKind;
14  import net.sf.saxon.s9api.XdmSequenceIterator;
15  
16  import org.daisy.pipeline.nlp.LangDetector;
17  import org.daisy.pipeline.nlp.calabash.impl.StringComposer.SentencePointer;
18  import org.daisy.pipeline.nlp.calabash.impl.StringComposer.TextPointer;
19  import org.daisy.pipeline.nlp.lexing.LexService.LexerInitException;
20  import org.daisy.pipeline.nlp.lexing.LexService.LexerToken;
21  import org.daisy.pipeline.nlp.lexing.LexService.Sentence;
22  
23  import com.xmlcalabash.util.TreeWriter;
24  
25  /**
26   * This class is used for rebuilding the input document with the additional XML
27   * elements resulting from the lexing, i.e. sentences and words.
28   * 
29   * The algorithm is run multiple times until no forbidden duplication is
30   * performed (this is the main reason why it can be slow sometimes). If the
31   * 'no-duplication-allowed' option is not enable, then only the nodes with ID
32   * will be kept under watch. Further improvements should forbid the algorithm to
33   * duplicate nodes attached to CSS properties such as "display: block", "border"
34   * or "cue-before".
35   * 
36   * The algorithm processes the inline sections on-the-fly as soon as they are
37   * detected by the InlineSectionFinder. Since the sections can be dispatched
38   * over distinct branches of the document tree, the tree is rebuilt by
39   * agglomerating tree paths with writeTree(), instead of the usual top-down
40   * recursive way whose building scope is too small for this purpose.
41   * 
42   * The levels are used for aligning the tree paths each other so that it is easy
43   * to find the common ancestor of a group of leaves.
44   * 
45   */
46  public class XmlBreakRebuilder implements InlineSectionProcessor {
47  
48  	private TreeWriter mTreeWriter;
49  	private Map<Locale, LexerToken> mLexers;
50  	private StringComposer mStringComposer;
51  	private FormatSpecifications mSpecs;
52  	private XdmNode mPreviousNode; //last node written
53  	private int mPreviousLevel;
54  	private DuplicationManager mDuplicationManager;
55  	private String mCurrentLang;
56  	private LangDetector mLangDetector;
57  	private List<String> mParsingErrors;
58  
59  	public XdmNode rebuild(TreeWriterFactory treeWriterFactory,
60  	        Map<Locale, LexerToken> lexers, XdmNode doc, FormatSpecifications specs,
61  	        LangDetector langDetector, boolean forbidAnyDup, List<String> parsingErrors)
62  	        throws LexerInitException {
63  		mLexers = lexers;
64  		mSpecs = specs;
65  		mLangDetector = langDetector;
66  		mStringComposer = new StringComposer();
67  		mPreviousNode = getRoot(doc);
68  		mPreviousLevel = 0;
69  		mParsingErrors = parsingErrors;
70  
71  		mDuplicationManager = new DuplicationManager(forbidAnyDup);
72  		Set<NodeInfo> unsplittable = new HashSet<NodeInfo>();
73  		List<NodeInfo> duplicated;
74  
75  		do {
76  			mDuplicationManager.onNewDocument();
77  			mTreeWriter = treeWriterFactory.newInstance();
78  			mTreeWriter.startDocument(doc.getBaseURI());
79  			XdmSequenceIterator iter = doc.axisIterator(Axis.CHILD);
80  			while (iter.hasNext()) {
81  				XdmNode n = (XdmNode) iter.next();
82  				if (n.getNodeKind() == XdmNodeKind.ELEMENT) {
83  
84  					XdmNode root = n;
85  					mPreviousNode = root;
86  					mTreeWriter.addStartElement(root);
87  					mTreeWriter.addAttributes(root);
88  					if (!"".equals(mSpecs.wordTag.getPrefix()))
89  						mTreeWriter.addNamespace(mSpecs.wordTag.getPrefix(),
90  						                         mSpecs.wordTag.getNamespaceURI());
91  					if (!"".equals(mSpecs.sentenceTag.getPrefix()))
92  						mTreeWriter.addNamespace(mSpecs.sentenceTag.getPrefix(),
93  						                         mSpecs.sentenceTag.getNamespaceURI());
94  
95  					new InlineSectionFinder().find(root, mPreviousLevel, specs, this,
96  					        unsplittable);
97  					closeAllElementsUntil(root, 0);
98  					break;
99  
100 				} else {
101 					mTreeWriter.addSubtree(n);
102 				}
103 			}
104 			mTreeWriter.endDocument();
105 
106 			duplicated = mDuplicationManager.getDuplicatedNodes();
107 
108 			unsplittable.addAll(duplicated);
109 
110 		} while (duplicated.size() > 0);
111 
112 		XdmNode result = mTreeWriter.getResult();
113 
114 		return result;
115 	}
116 
117 	public static int getDuplicatedIDs(Map<String, Integer> ref, Map<String, Integer> actual,
118 	        Set<String> unsplittable) {
119 		int res = 0;
120 		for (Map.Entry<String, Integer> e : ref.entrySet()) {
121 			if (e.getValue() == 1 && !(Integer.valueOf(1).equals(actual.get(e.getKey())))) {
122 				unsplittable.add(e.getKey());
123 				++res;
124 			}
125 		}
126 
127 		return res;
128 	}
129 
130 	private void addOneWord(TextPointer word, List<Leaf> leaves, List<String> text) {
131 		Leaf wordParent = deepestAncestorOf(leaves, word.firstSegment, word.lastSegment);
132 		if (wordParent.formatting == null)
133 			return;
134 
135 		startLexingElement(wordParent, mSpecs.wordTag);
136 		if (word.firstSegment == word.lastSegment) {
137 			writeTree(leaves.get(word.firstSegment), text.get(word.firstSegment).substring(
138 			        word.firstIndex, word.lastIndex));
139 		} else {
140 			writeTree(leaves.get(word.firstSegment), text.get(word.firstSegment).substring(
141 			        word.firstIndex));
142 			for (int i = word.firstSegment + 1; i < word.lastSegment; ++i)
143 				writeTree(leaves.get(i), text.get(i));
144 			writeTree(leaves.get(word.lastSegment), text.get(word.lastSegment).substring(0,
145 			        word.lastIndex));
146 		}
147 		closeAllElementsUntil(wordParent.formatting, 1);
148 	}
149 
150 	@Override
151 	public void onInlineSectionFound(List<Leaf> leaves, List<String> text, Locale lang)
152 	        throws LexerInitException {
153 
154 		Locale expectedLang = lang;
155 		lang = mLangDetector.findLang(lang, text);
156 		if (lang != null && expectedLang != null) {
157 			if (expectedLang.getLanguage().equals(lang.getLanguage()))
158 				mCurrentLang = null; //this indicates that there is no need to add xml:lang attributes
159 			else
160 				mCurrentLang = lang.getISO3Language(); //is it the right standard?
161 		} else
162 			mCurrentLang = null;
163 
164 		LexerToken lexer = mLexers.get(lang);
165 		if (lexer == null) {
166 			lexer = mLexers.get(null); //a generic lexer is always provided
167 		}
168 
169 		String input = mStringComposer.concat(text);
170 		List<Sentence> sentences = lexer.split(input, lang, mParsingErrors);
171 
172 		boolean[] isLexMark = new boolean[leaves.size()];
173 		for (int k = 0; k < isLexMark.length; ++k)
174 			isLexMark[k] = (leaves.get(k).formatting == null);
175 		List<SentencePointer> pointers = mStringComposer.makePointers(sentences, text,
176 		        isLexMark);
177 
178 		mDuplicationManager.onNewSection();
179 
180 		int sSegBoundary = -1;
181 		int sIndexBoundary = -1;
182 		for (SentencePointer sp : pointers) {
183 			//Gap between the previous sentence and the current sentence
184 			fillGap(sSegBoundary, sIndexBoundary, sp.boundaries.firstSegment,
185 			        sp.boundaries.firstIndex, leaves, text);
186 
187 			sSegBoundary = sp.boundaries.lastSegment;
188 			sIndexBoundary = sp.boundaries.lastIndex;
189 			Leaf sentenceParent = deepestAncestorOf(leaves, sp.boundaries.firstSegment,
190 			        sp.boundaries.lastSegment);
191 
192 			if (sentenceParent.formatting == null) {
193 				//it can happen if the section is composed only of punctuation
194 				//marks inserted to help the Lexer
195 				continue;
196 			}
197 
198 			startLexingElement(sentenceParent, mSpecs.sentenceTag);
199 			int wSegBoundary = sp.boundaries.firstSegment;
200 			int wIndexBoundary = sp.boundaries.firstIndex;
201 			if (sp.content != null)
202 				for (TextPointer w : sp.content) {
203 					//Gap between the previous word and the current word
204 					fillGap(wSegBoundary, wIndexBoundary, w.firstSegment, w.firstIndex,
205 					        leaves, text);
206 					wSegBoundary = w.lastSegment;
207 					wIndexBoundary = w.lastIndex;
208 					addOneWord(w, leaves, text);
209 				}
210 			//Gap between the last words and the end of the sentence
211 			fillGap(wSegBoundary, wIndexBoundary, sp.boundaries.lastSegment,
212 			        sp.boundaries.lastIndex, leaves, text);
213 
214 			closeAllElementsUntil(sentenceParent.formatting, 1);
215 
216 		}
217 		//Gap between the last sentence and the end of the section
218 		fillGap(sSegBoundary, sIndexBoundary, -1, -1, leaves, text);
219 		mCurrentLang = null;
220 	}
221 
222 	@Override
223 	public void onEmptySectionFound(List<Leaf> leaves) {
224 		mDuplicationManager.onNewSection();
225 		for (int i = 0; i < leaves.size(); ++i) {
226 			writeTree(leaves.get(i), null);
227 		}
228 	}
229 
230 	private void addNode(XdmNode node, int level) {
231 		switch (node.getNodeKind()) {
232 		case ELEMENT:
233 			mDuplicationManager.onNewNode(node, level);
234 			mTreeWriter.addStartElement(node);
235 			mTreeWriter.addAttributes(node);
236 			mPreviousLevel = level;
237 			mPreviousNode = node;
238 			break;
239 		case COMMENT:
240 			mTreeWriter.addComment(node.getStringValue());
241 			mPreviousLevel = level - 1;
242 			mPreviousNode = node.getParent();
243 			break;
244 		case PROCESSING_INSTRUCTION:
245 			mTreeWriter.addPI(node.getNodeName().getClarkName(), node.getStringValue());
246 			mPreviousLevel = level - 1;
247 			mPreviousNode = node.getParent();
248 			break;
249 		default:
250 			mTreeWriter.addSubtree(node);
251 			mPreviousLevel = level - 1;
252 			mPreviousNode = node.getParent();
253 			break;
254 		}
255 	}
256 
257 	private void startLexingElement(Leaf wordOrsentenceParent, QName elementToWrite) {
258 		XdmNode[] path = new XdmNode[wordOrsentenceParent.level + 1];
259 
260 		//*** Find the deepest common ancestor. ***
261 		//Also, keep track of the path between the ancestor and the sentence's parent.
262 		XdmNode sentenceAncestor = wordOrsentenceParent.formatting;
263 		XdmNode lastNodeAncestor = mPreviousNode;
264 		int minLevel = Math.min(wordOrsentenceParent.level, mPreviousLevel);
265 		int topLevel = wordOrsentenceParent.level;
266 		for (; topLevel > minLevel; --topLevel) {
267 			path[topLevel] = sentenceAncestor;
268 			sentenceAncestor = sentenceAncestor.getParent();
269 		}
270 		for (int lastLevel = mPreviousLevel; lastLevel > minLevel; --lastLevel)
271 			lastNodeAncestor = lastNodeAncestor.getParent();
272 
273 		for (; !same(sentenceAncestor, lastNodeAncestor); --topLevel) {
274 			path[topLevel] = sentenceAncestor;
275 			sentenceAncestor = sentenceAncestor.getParent();
276 			lastNodeAncestor = lastNodeAncestor.getParent();
277 		}
278 
279 		//*** Close the elements between the last written elements and the found common ancestor ***
280 		// Sentences and words have already been closed by closeAllElementsUntil()
281 		while (!same(mPreviousNode, lastNodeAncestor)) {
282 			mTreeWriter.addEndElement();
283 			mPreviousNode = mPreviousNode.getParent();
284 		}
285 
286 		//*** Add all the elements on the path between the sentence/word and the ancestor ***
287 		for (int l = topLevel + 1; l <= wordOrsentenceParent.level; ++l) {
288 			XdmNode n = path[l];
289 			addNode(n, l);
290 		}
291 
292 		//*** Create the element ***
293 		mTreeWriter.addStartElement(elementToWrite);
294 		if (mCurrentLang != null) {
295 			//the lang attribute will be dispatched later when the format-compliant
296 			//elements will be created.
297 			mTreeWriter.addAttribute(mSpecs.langAttr, mCurrentLang);
298 		}
299 		mPreviousLevel = wordOrsentenceParent.level;
300 	}
301 
302 	private void writeTree(Leaf leaf, String text) {
303 		if (text != null && text.isEmpty())
304 			return; //the fillGap can call writeTree with empty strings
305 
306 		if (leaf.formatting == null)
307 			return; //this happens when punctuation marks has been added to help the Lexer
308 
309 		XdmNode[] leafPath = new XdmNode[leaf.level + 1];
310 
311 		//*** Find the deepest common ancestor. ***
312 		//Also, keep track of the path between the ancestor and the leaf.
313 		XdmNode leafAncestor = leaf.formatting;
314 		XdmNode lastNodeAncestor = mPreviousNode;
315 		int minLevel = Math.min(leaf.level, mPreviousLevel);
316 		int topLevel = leaf.level;
317 		for (; topLevel > minLevel; --topLevel) {
318 			leafPath[topLevel] = leafAncestor;
319 			leafAncestor = leafAncestor.getParent();
320 		}
321 		for (int lastLevel = mPreviousLevel; lastLevel > minLevel; --lastLevel)
322 			lastNodeAncestor = lastNodeAncestor.getParent();
323 
324 		for (; !same(leafAncestor, lastNodeAncestor); --topLevel) {
325 			leafPath[topLevel] = leafAncestor;
326 			leafAncestor = leafAncestor.getParent();
327 			lastNodeAncestor = lastNodeAncestor.getParent();
328 		}
329 
330 		//*** Close the elements between the last written elements and the found common ancestor ***
331 		// Sentences and words have already been closed by closeAllElementsUntil()
332 		while (!same(mPreviousNode, lastNodeAncestor)) {
333 			mTreeWriter.addEndElement();
334 			mPreviousNode = mPreviousNode.getParent();
335 		}
336 
337 		//*** Open the new elements between the common ancestor (already opened) and the leaf (included) ***
338 		for (int l = topLevel + 1; l <= leaf.level; ++l) {
339 			XdmNode n = leafPath[l];
340 			addNode(n, l);
341 		}
342 
343 		if (text != null) {
344 			mTreeWriter.addText(text);
345 			mPreviousLevel = leaf.level;
346 		}
347 
348 	}
349 
350 	/**
351 	 * @param dest is not closed.
352 	 * @param extra is used to close additional nodes like sentences or words
353 	 */
354 	private void closeAllElementsUntil(XdmNode dest, int extra) {
355 		while (!same(mPreviousNode, dest)) {
356 			mTreeWriter.addEndElement();
357 			mPreviousNode = mPreviousNode.getParent();
358 			--mPreviousLevel;
359 		}
360 		for (; extra > 0; --extra) {
361 			mTreeWriter.addEndElement();
362 		}
363 	}
364 
365 	private void fillGap(int leftSegment, int leftIndex, int rightSegment, int rightIndex,
366 	        List<Leaf> leaves, List<String> text) {
367 		if (leftSegment == -1) {
368 			for (leftSegment = 0; leftSegment < text.size() && text.get(leftSegment) == null; ++leftSegment) {
369 				writeTree(leaves.get(leftSegment), null);
370 			}
371 			leftIndex = 0;
372 		}
373 
374 		int formerRightSegment = rightSegment;
375 		if (rightSegment == -1) {
376 			rightSegment = text.size() - 1;
377 			formerRightSegment = rightSegment;
378 			for (; rightSegment >= 0 && text.get(rightSegment) == null; --rightSegment);
379 			if (rightSegment >= 0) {
380 				rightIndex = text.get(rightSegment).length();
381 			}
382 		}
383 
384 		if (leftSegment < rightSegment) {
385 			writeTree(leaves.get(leftSegment), text.get(leftSegment).substring(leftIndex));
386 
387 			for (int k = leftSegment + 1; k < rightSegment; ++k)
388 				writeTree(leaves.get(k), text.get(k));
389 
390 			writeTree(leaves.get(rightSegment), text.get(rightSegment)
391 			        .substring(0, rightIndex));
392 		} else if (leftSegment == rightSegment && rightIndex > leftIndex) {
393 			writeTree(leaves.get(leftSegment), text.get(leftSegment).substring(leftIndex,
394 			        rightIndex));
395 		}
396 
397 		for (++rightSegment; rightSegment <= formerRightSegment; ++rightSegment) {
398 			writeTree(leaves.get(rightSegment), null);
399 		}
400 	}
401 
402 	//the return object is not really a Tree Leaf but it is convenient to carry the node's level
403 	private Leaf deepestAncestorOf(List<Leaf> leaves, int from, int to) {
404 		if (from == to) {
405 			return leaves.get(from);
406 		}
407 
408 		int minLevel = Integer.MAX_VALUE;
409 		for (int k = from; k <= to; ++k)
410 			if (leaves.get(k).level < minLevel && leaves.get(k).formatting != null)
411 				minLevel = leaves.get(k).level;
412 
413 		XdmNode[] all = new XdmNode[leaves.size()];
414 		int size = 0;
415 		for (int i = from; i <= to; ++i) {
416 			all[size] = leaves.get(i).formatting;
417 			if (all[size] != null) { //it can be null when the segment is added to help the Lexer
418 				for (int k = leaves.get(i).level - minLevel; k > 0; --k)
419 					all[size] = all[size].getParent();
420 				++size;
421 			}
422 		}
423 
424 		while (true) {
425 			int i = 1;
426 			for (; i < size && same(all[i], all[0]); ++i);
427 			if (i >= size) {
428 				//ancestor found
429 				break;
430 			}
431 			for (i = 0; i < size; ++i)
432 				all[i] = all[i].getParent();
433 			--minLevel;
434 		}
435 
436 		Leaf result = new Leaf();
437 		result.formatting = all[0];
438 		result.level = minLevel;
439 
440 		return result;
441 	}
442 
443 	private static XdmNode getRoot(XdmNode node) {
444 		XdmSequenceIterator iter = node.axisIterator(Axis.CHILD);
445 		while (iter.hasNext()) {
446 			XdmNode n = (XdmNode) iter.next();
447 			if (n.getNodeKind() == XdmNodeKind.ELEMENT)
448 				return n;
449 		}
450 		return null;
451 	}
452 
453 	private static boolean same(XdmNode n1, XdmNode n2) {
454 		return ((n1 == null && n2 == null) || (n1 != null && n2 != null && n1
455 		        .getUnderlyingNode().isSameNodeInfo(n2.getUnderlyingNode())));
456 	}
457 }