1
2
3
4
5
6
7
8
9
10
11
12 package sk.uniba.euromath.document;
13 import java.io.IOException;
14 import java.util.Collection;
15 import java.util.EnumSet;
16 import java.util.concurrent.locks.ReentrantLock;
17 import javax.xml.XMLConstants;
18 import javax.xml.namespace.QName;
19 import org.w3c.dom.Attr;
20 import org.w3c.dom.DocumentFragment;
21 import org.w3c.dom.Element;
22 import org.w3c.dom.Node;
23 import org.w3c.dom.Text;
24 import sk.baka.ikslibs.DOMUtils;
25 import sk.baka.ikslibs.interval.DOMInterval;
26 import sk.baka.ikslibs.levelmapper.NodeListID;
27 import sk.baka.ikslibs.modify.DOMMutils;
28 import sk.baka.ikslibs.ptr.DOMPoint;
29 import sk.baka.ikslibs.ptr.DomPointer;
30 import sk.baka.ikslibs.ptr.DomPointerFactory;
31 import sk.baka.ikslibs.ptr.DomPointerFlag;
32 import sk.baka.ikslibs.ref.EntityManager;
33 import sk.baka.xml.gene.ExportException;
34 import sk.uniba.euromath.Const;
35 import sk.uniba.euromath.tools.StringTools;
36 /***
37 * <p>
38 * Serves for modifying document. Tracks changes in document, and makes them
39 * consistent with 'splitted' images of document. Accessible from out of
40 * document package. All functions work on DOM-level nodes. Thread safe.
41 * </p>
42 * <p>
43 * This class is unchecked - it allows to set given text or modify document
44 * structure even if the change violates the grammar rules. Caller should make
45 * sure that the document is valid before the transformation execution.
46 * </p>
47 * <p>
48 * WARNING: when modifying the document, then IDs pointing to nodes after node
49 * being modified can become invalid (may denote a wrong node) - this happens
50 * for example when the text/cdata/comment/pi node is deleted (for example, when
51 * <code>null</code> value is given to a <code>setText()</code> function).
52 * </p>
53 * @author Martin Vysny
54 */
55 public final class DocumentModifier {
56 /***
57 * Document.
58 */
59 private final DomCore doc;
60 /***
61 * Collections containing all opened views.
62 */
63 private final Collection< ? extends DocumentView> views;
64 /***
65 * Listens to the events.
66 */
67 private final DocumentListeners listeners;
68 /***
69 * This lock is locked when some thread starts the document modification,
70 * and released when a thread finishes the modification. When the lock is
71 * released, the document is transformed if there are no more threads
72 * waiting for the modification.
73 */
74 private final ReentrantLock currentModifierLock = new ReentrantLock();
75 /***
76 * Constructs instance of document modifier.
77 * @param doc modifies this document.
78 * @param views collection of views, opened for this document. This table is
79 * managed, and modified, by XMLAccess, DocumentModifier class doesn't
80 * modify it.
81 * @param listeners listens for document modifications
82 */
83 DocumentModifier(DomCore doc, Collection< ? extends DocumentView> views,
84 DocumentListeners listeners) {
85 super();
86 this.doc = doc;
87 this.views = views;
88 this.listeners = listeners;
89 }
90 /***
91 * <p>
92 * Begins modification sequence. All modifications to document must be
93 * enclosed with <code>startModify() .. endModify()</code> sequence. In
94 * other words, modifying document when <code>isModifying()==false</code>
95 * will result in <code>IllegalStateException</code>.
96 * <code>startModify()</code> calls can be nested, i.e. it is possible to
97 * call <code>startModify()</code> when <code>isModifying()</code>, but
98 * every call to <code>startModify()</code> should be closed with
99 * <code>endModify()</code>, otherwise document won't be never
100 * transformed, and other threads will block forever.
101 * </p>
102 * <p>
103 * Two threads cannot modify one document concurrently. If one thread is
104 * currently modifying a document and second thread calls
105 * <code>startModify()</code>, the function will block until current
106 * thread finishes the modification.
107 * </p>
108 */
109 public void startModify() {
110 currentModifierLock.lock();
111
112 }
113 /***
114 * Ends modification of document. It may still be possible to edit document,
115 * when every call to startModify() wasn't yet accompanied by endModify()
116 * call. When last startModify() is closed by this call, transformation is
117 * performed.
118 * @throws ExportException when something goes wrong in the process of
119 * transformation.
120 */
121 public void endModify() throws ExportException {
122 ensureModify();
123 final boolean hasWaitingModifiers = currentModifierLock
124 .hasQueuedThreads();
125 final boolean unlocksLastLock = currentModifierLock.getHoldCount() <= 1;
126
127 currentModifierLock.unlock();
128
129
130
131 if (hasWaitingModifiers)
132 return;
133 if (unlocksLastLock) {
134
135 if (doc.getSplittedChanges().hasChanges()) {
136 for (final DocumentView view : views) {
137 try {
138 view.export();
139 } catch (IOException ex) {
140 throw new ExportException("I/O error: "
141 + ex.getLocalizedMessage(), ex);
142 }
143 }
144 doc.getSplittedChanges().clear();
145 listeners.documentWasTransformed();
146 }
147 }
148 }
149 /***
150 * Returns <code>true</code>, if document is being modified by caller
151 * thread.
152 * @return <code>true</code>, if document is being modified,
153 * <code>false</code> otherwise.
154 */
155 public boolean isModifying() {
156 return currentModifierLock.isHeldByCurrentThread();
157 }
158 /***
159 * Ensures that document is being modified by caller thread. If not,
160 * <code>IllegalStateException</code> is thrown.
161 */
162 void ensureModify() {
163 if (!isModifying())
164 throw new IllegalStateException(
165 "Document is not currently being modified by this thread.");
166 }
167 /***
168 * Sets text, denoted by given id, to given value. It may be pi, comment or
169 * attribute node.
170 * @param id id of text node, that receives new value.
171 * @param value new text value. <code>null</code> value is equivalent to
172 * an empty string.
173 * @deprecated
174 */
175 @Deprecated
176 public void setText(String id, String value) {
177 NodeListID list = doc.getIDManager().getNode(id);
178 DOMMutils.setText(list, value);
179 }
180 /***
181 * Sets text of given node to given value. It may be pi, comment, attribute,
182 * text or cdata node only.
183 * @param node the text node, that receives new value.
184 * @param value new text value. <code>null</code> value is equivalent to
185 * an empty string.
186 */
187 public void setText(Node node, String value) {
188 checkReserved(node);
189 DOMMutils.setText(node, value);
190 }
191 /***
192 * Splits the text/cdata node at the specified position. If pos is equal to
193 * zero or length of textual value of given text then nothing happens. The
194 * node denoted by given id is not moved, only its textual value is changed.
195 * @param id the id of the text/cdata node
196 * @param pos zero-based position where the text shall be splitted.
197 * @return a second half of the splitted node. It may be <code>null</code>
198 * when no splitting occured.
199 * @deprecated
200 */
201 @Deprecated
202 public Text splitText(String id, int pos) {
203 NodeListID list = doc.getIDManager().getNode(id);
204 return DOMMutils.splitText(list.getPointer(pos, true, true));
205 }
206 /***
207 * Splits the text/cdata node at the specified position. If pos is equal to
208 * zero or length of textual value of given text then nothing happens. The
209 * node denoted by given id is not moved, only its textual value is changed.
210 * @param text the text/cdata node
211 * @param pos zero-based position where the text shall be splitted.
212 * @return a second half of the splitted node. It may be <code>null</code>
213 * when no splitting occured.
214 * @deprecated
215 */
216 @Deprecated
217 public Text splitText(Text text, int pos) {
218 return DOMMutils.splitText(text, pos);
219 }
220 /***
221 * Splits the text/cdata node at the specified position. If pos is equal to
222 * zero or length of textual value of given text then nothing happens. The
223 * node denoted by given id is not moved, only its textual value is changed.
224 * @param ptr the pointer pointing into the text/cdata node.
225 * @return a second half of the splitted node. It may be <code>null</code>
226 * when no splitting occured.
227 * @deprecated
228 */
229 @Deprecated
230 public Text splitText(DomPointer ptr) {
231 return DOMMutils.splitText(ptr);
232 }
233 /***
234 * Inserts text into element, depending on given parameters. If ip points
235 * into text, then that text is modified instead. If ip points to a location
236 * next to node with equal type, then that text is modified instead.
237 * @param ip insert point, where to insert new text.
238 * @param contextId the context element, where new text must be inserted.
239 * @param value new text value. if <code>null</code> or empty string ("")
240 * is given, then nothing is created.
241 * @param type type of created node, one of <code>Node.TEXT_NODE</code> or
242 * <code>Node.CDATA_SECTION_NODE</code> constants. When modifying text,
243 * this parameter is ignored.
244 * @return id of newly created text element, or id of modified element. If
245 * value is empty or null, then <code>null</code> is returned.
246 * @throws DocumentException when element with given id doesn't exist or it
247 * doesn't denote regular element.
248 * @deprecated
249 */
250 @Deprecated
251 public String insertText(DOMPoint ip, String contextId, String value,
252 short type) throws DocumentException {
253 return insertText(ip, doc.getContent().getElement(contextId), value,
254 type);
255 }
256 /***
257 * Inserts text into element, depending on given parameters. If ip points
258 * into text, then that text is modified instead. If ip points to a location
259 * next to node with equal type, then that text is modified instead.
260 * @param point insert point, where to insert new text. It must not point
261 * into an entity.
262 * @param value new text value. if <code>null</code> or empty string ("")
263 * is given, then nothing is created.
264 * @param type type of created node, one of <code>Node.TEXT_NODE</code> or
265 * <code>Node.CDATA_SECTION_NODE</code> constants. When modifying text,
266 * this parameter is ignored.
267 * @return id of newly created text element, or id of modified element. If
268 * value is empty or null, then <code>null</code> is returned.
269 * @deprecated
270 */
271 @Deprecated
272 public String insertText(final DomPointer point, final String value,
273 short type) {
274 final Node n= DOMMutils.insertText(point, value, type);
275 if(n==null)return null;
276 return doc.getIDManager().getID(n);
277 }
278 /***
279 * Inserts text into element, depending on given parameters. If ip points
280 * into text, then that text is modified instead. If ip points to a location
281 * next to node with equal type, then that text is modified instead.
282 * @param ip insert point, where to insert new text.
283 * @param contextElement the context element, where new text must be
284 * inserted.
285 * @param value new text value. if <code>null</code> or empty string ("")
286 * is given, then nothing is created.
287 * @param type type of created node, one of <code>Node.TEXT_NODE</code> or
288 * <code>Node.CDATA_SECTION_NODE</code> constants. When modifying text,
289 * this parameter is ignored.
290 * @return id of newly created text element, or id of modified element. If
291 * value is empty or null, then <code>null</code> is returned.
292 * @deprecated
293 */
294 @Deprecated
295 public String insertText(DOMPoint ip, Element contextElement,
296 String value, short type) {
297 final Node n= DOMMutils.insertText(ip, contextElement, value, type);
298 if(n==null)return null;
299 return doc.getIDManager().getID(n);
300 }
301 /***
302 * Removes node(s) with given id from document.
303 * @param id id of the node, that will be removed.
304 * @deprecated
305 */
306 @Deprecated
307 public void remove(String id) {
308 final NodeListID list = doc.getIDManager().getNode(id);
309 if (!list.isTextual()) {
310
311 remove(list.item(0));
312 return;
313 }
314
315 remove(list.getStart(), list.getEnd());
316 }
317 /***
318 * Removes given node from the document. If both previous and next sibling
319 * of the node are of same text type (text or cdata) then these nodes are
320 * merged into one.
321 * @param node the node, that will be removed. Its id is removed aswell, but
322 * the id of descendants is not removed.
323 */
324 public void remove(Node node) {
325 checkReserved(node);
326 DOMMutils.remove(node);
327 }
328 /***
329 * Check if such node is reserved and cannot be created in the document.
330 * @param node node to check.
331 * @throws IllegalArgumentException if node is reserved.
332 */
333 private void checkReserved(final Node node) {
334 checkReserved(node.getNamespaceURI(), node.getNodeType());
335 }
336 /***
337 * Check if such node is reserved and cannot be created in the document.
338 * @param namespace the namespace of the node
339 * @param nodeType the type of the node.
340 * @throws IllegalArgumentException if node is reserved.
341 */
342 private void checkReserved(final String namespace, final short nodeType) {
343 if ((nodeType != Node.ATTRIBUTE_NODE)
344 && (nodeType != Node.ELEMENT_NODE))
345 return;
346 if (StringTools.nonNullStr(namespace).startsWith(Const.EM_URI))
347 throw new IllegalArgumentException(
348 "Namespace " + Const.EM_URI + " is reserved.");
349 }
350 /***
351 * Creates an attribute. If attribute with given local name and namespace is
352 * already present in context node, its value is modified instead - prefix
353 * is not changed.
354 * @param id id of element, that contains the attribute.
355 * @param prefix prefix of qname of attribute. See
356 * <code>NamespaceManager</code> for details.
357 * @param localName local name of attribute.
358 * @param namespace namespace of attribute.
359 * @param value new value for attribute.
360 * @return ID of newly created attribute.
361 */
362 public String createAttribute(String id, String prefix, String localName,
363 String namespace, String value) {
364 return createAttribute(doc.getIDManager().getElement(id), prefix,
365 localName, namespace, value);
366 }
367 /***
368 * Creates an attribute. If attribute with given local name and namespace is
369 * already present in context node, its value is modified instead - prefix
370 * is not changed.
371 * @param id id of element, that contains the attribute.
372 * @param qname qualified name of the attribute
373 * @param value new value for attribute.
374 * @return ID of newly created attribute.
375 */
376 public String createAttribute(String id, QName qname, String value)
377 {
378 return createAttribute(id, qname.getPrefix(), qname.getLocalPart(),
379 qname.getNamespaceURI(), value);
380 }
381 /***
382 * Creates an attribute. If attribute with given local name and namespace is
383 * already present in context node, its value is modified instead - prefix
384 * is not changed.
385 * @param e element, that contains the attribute.
386 * @param qname qualified name of the attribute
387 * @param value new value for attribute.
388 * @return ID of newly created attribute.
389 */
390 public String createAttribute(Element e, QName qname, String value) {
391 return createAttribute(e, qname.getPrefix(), qname.getLocalPart(),
392 qname.getNamespaceURI(), value);
393 }
394 /***
395 * Creates an attribute. If attribute with given local name and namespace is
396 * already present in context node, its value is modified instead - prefix
397 * is not changed.
398 * @param e element, that contains the attribute.
399 * @param prefix prefix of qname of attribute. See
400 * <code>NamespaceManager</code> for details.
401 * @param localName local name of attribute.
402 * @param namespace namespace of attribute.
403 * @param value new value for attribute.
404 * @return ID of newly created attribute.
405 */
406 public String createAttribute(Element e, String prefix, String localName,
407 String namespace, String value) {
408
409 checkReserved(namespace, Node.ATTRIBUTE_NODE);
410
411 if (DOMUtils.equalsURI(namespace, e.getNamespaceURI())) {
412 namespace = XMLConstants.NULL_NS_URI;
413 prefix = XMLConstants.DEFAULT_NS_PREFIX;
414 }
415 final Attr attr=DOMMutils.createAttribute(e, prefix, localName, namespace, value);
416 return doc.getIDManager().getID(attr);
417 }
418 /***
419 * Inserts the node at the specified pointer. The function does not use the
420 * ip part of the pointer hence it inserts node into correct place even if
421 * there was a node already inserted before this place. IP part is used only
422 * if merge is true and the node is a text or cdata node. If the node is an
423 * element then it receives an id, however no descendant elements receives
424 * an id.
425 * @param node the node to insert, it must be element, comment, pi, text or
426 * cdata only.
427 * @param ptr where to insert the node.
428 * @param merge if <code>true</code> then if new node is a text node then
429 * it will be merged with adjacent nodes if possible. If <code>false</code>
430 * then no merging is performed.
431 * @deprecated
432 */
433 @Deprecated
434 public void insertNode(Node node, DomPointer ptr, boolean merge) {
435 DOMMutils.insertNode(node, ptr, merge);
436 }
437 /***
438 * Inserts all nodes from specified document fragment into specified
439 * position. All nodes are automatically merged. All <code>emp:id</code>
440 * attributes are ignored. All nodes are removed from the fragment in the
441 * process and become part of the document.
442 * @param frag the fragment
443 * @param ptr where to insert nodes from the fragment.
444 * @deprecated
445 */
446 @Deprecated
447 public void insertFragment(DocumentFragment frag, DomPointer ptr) {
448 DOMMutils.insertFragment(frag, ptr);
449 }
450 /***
451 * Replaces the node with another node. <code>newNode</code> must have
452 * <code>null</code> parent or it must be from another document.
453 * @param node the node to be replaced.
454 * @param newNode the node will be replaced with this node. If
455 * <code>null</code> then given node is removed. If not from our document
456 * then it is automatically imported.
457 */
458 public void replaceNode(Node node, Node newNode) {
459 if (newNode == null) {
460 remove(node);
461 return;
462 }
463 doc.checkNode(node);
464 if ((newNode.getParentNode() != null) && (doc.isOurNode(newNode)))
465 throw new IllegalArgumentException(
466 "The replacing node is present in our document");
467 ensureModify();
468
469 final Node nextSibling = node.getNextSibling();
470 final Node parent = node.getParentNode();
471 parent.removeChild(node);
472
473 Node replaceBy = newNode;
474 if (!doc.isOurNode(replaceBy))
475 replaceBy = doc.getDocument().importNode(newNode, true);
476 DOMMutils.insertNode(replaceBy, DomPointerFactory.create(nextSibling), true);
477 }
478 /***
479 * Checks whether all nodes from specified range can be deleted - i.e. if
480 * <code>delete</code> with these parameters shall succeed.
481 * @param from start of the removal interval. The node where this pointer
482 * points shall be removed (an exception is if some contents of the node are
483 * left after the operation).
484 * @param to end of the removal interval.
485 * @return <code>true</code> if the specified interval is removable,
486 * <code>false</code> otherwise.
487 */
488 public boolean isRemovable(DomPointer from, DomPointer to) {
489
490
491
492 if (to != null)
493 to = to.getNext(EnumSet.of(DomPointerFlag.NotIntoEntity,
494 DomPointerFlag.PointToNode), true, false,false);
495 if (to != null)
496 to = to.skipFromEntities();
497 if (to == null)
498 to = DomPointerFactory.getLast(doc.getDocument(),
499 EnumSet.of(DomPointerFlag.NotIntoEntity));
500
501
502 if (to.inEntity())
503 return false;
504
505 from = from.getNext(EnumSet.of(DomPointerFlag.NotIntoEntity,
506 DomPointerFlag.PointToNode), true, false,false);
507 if (from != null)
508 to = to.skipFromEntities();
509 if (from == null) {
510
511
512 return true;
513 }
514
515
516 if (from.inEntity())
517 return false;
518 if (from.equals(to))
519 return true;
520 if (from.compareTo(to) > 0)
521 throw new IllegalArgumentException("'from' must be less than 'to'");
522 return true;
523 }
524 /***
525 * Tries to remove all nodes in the specified range (the node pointed to by
526 * <code>to</code> parameter is not removed). The function shall not
527 * remove elements that shall not have all its children removed.
528 * @param from start of the removal interval. The node where this pointer
529 * points shall be removed (exception is a node which have some children
530 * left after the operation).
531 * @param to end of the removal interval. It may be <code>null</code>- in
532 * such case the function shall remove all nodes to the end of the document.
533 * @deprecated
534 */
535 @Deprecated
536 public void remove(DomPointer from, DomPointer to) {
537 new DOMInterval(from,to).toRange().deleteContents();
538 }
539 /***
540 * Tries to remove all nodes in the specified range (the node pointed to by
541 * <code>to</code> parameter is not removed). Removed nodes are returned
542 * in a <code>DocumentFragment</code> instance. Both pointers must have
543 * the same parent element.
544 * @param from start of the removal interval. The node where this pointer
545 * points shall be removed.
546 * @param to end of the removal interval.
547 * @return <code>DocumentFragment</code> instance containing nodes that
548 * have been cut from the document. Never <code>null</code>. No element
549 * in the target fragment will have <code>emp:id</code> attribute.
550 * @deprecated
551 */
552 @Deprecated
553 public DocumentFragment cut(DomPointer from, DomPointer to) {
554 return new DOMInterval(from,to).toRange().extractContents();
555 }
556 /***
557 * Tries to insert some text into specified position.
558 * @param list current list of nodes, where to insert the text.
559 * @param pos the position in the <code>textValue</code> string.
560 * @param type type of created node, one of <code>Node.TEXT_NODE</code> or
561 * <code>Node.CDATA_SECTION_NODE</code> constants. When modifying text,
562 * this parameter is ignored.
563 * @param text the text to be inserted
564 * @return new instance of list that reflects the changes made to the
565 * document.
566 * @throws IllegalArgumentException if no text can be inserted at specified
567 * position.
568 * @deprecated
569 */
570 @Deprecated
571 public NodeListID insertText(NodeListID list, int pos, String text,
572 short type) {
573 DOMMutils.insertText(list, pos, text, type);
574 NodeListID result = list.recover();
575 return result;
576 }
577 /***
578 * Inserts an entity into specified position.
579 * @param list current list of nodes, where to insert the entity.
580 * @param pos the position in the <code>textValue</code> string.
581 * @param entityName the name of the entity.
582 * @return new instance of list that reflects the changes made to the
583 * document.
584 * @deprecated
585 */
586 @Deprecated
587 public NodeListID insertEntity(NodeListID list, int pos, String entityName) {
588 final EntityManager em = doc.getEntityManager();
589 DOMMutils.insertEntity(list, pos, entityName, em);
590 final NodeListID result = list.recover();
591 return result;
592 }
593 /***
594 * Tries to delete specified range of text.
595 * @param list list of text/cdata nodes. If function finishes succesfully
596 * and the document gets modified, this list is invalid and must not be
597 * used.
598 * @param fromPos the starting position
599 * @param toPos the ending position - character at this position shall not
600 * be deleted.
601 * @return new instance of list that reflects the changes made to the
602 * document.
603 * @deprecated
604 */
605 @Deprecated
606 public NodeListID delete(NodeListID list, int fromPos, int toPos) {
607 DOMMutils.delete(list, fromPos, toPos);
608 final NodeListID result = list.recover();
609 return result;
610 }
611 }