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.concurrent.locks.ReentrantLock;
16 import javax.xml.XMLConstants;
17 import javax.xml.namespace.QName;
18 import org.w3c.dom.Attr;
19 import org.w3c.dom.Element;
20 import org.w3c.dom.Node;
21 import sk.baka.ikslibs.DOMUtils;
22 import sk.baka.ikslibs.modify.DOMMutils;
23 import sk.baka.ikslibs.ptr.DomPointerFactory;
24 import sk.baka.xml.gene.ExportException;
25 import sk.baka.xml.gene.ExportUtils;
26 /***
27 * <p>
28 * Serves for modifying document. Tracks changes in document, and makes them
29 * consistent with 'splitted' images of document. Accessible from out of
30 * document package. All functions work on DOM-level nodes. Thread safe.
31 * </p>
32 * <p>
33 * This class is unchecked - it allows to set given text or modify document
34 * structure even if the change violates the grammar rules. Caller should make
35 * sure that the document is valid before the transformation execution.
36 * </p>
37 * <p>
38 * WARNING: when modifying the document, then IDs pointing to nodes after node
39 * being modified can become invalid (may denote a wrong node) - this happens
40 * for example when the text/cdata/comment/pi node is deleted (for example, when
41 * <code>null</code> value is given to a <code>setText()</code> function).
42 * </p>
43 * @author Martin Vysny
44 */
45 public final class DocumentModifier {
46 /***
47 * Document.
48 */
49 private final XMLAccess doc;
50 /***
51 * Collections containing all opened views.
52 */
53 private final Collection< ? extends DocumentView> views;
54 /***
55 * Listens to the events.
56 */
57 private final DocumentListeners listeners;
58 /***
59 * This lock is locked when some thread starts the document modification,
60 * and released when a thread finishes the modification. When the lock is
61 * released, the document is transformed if there are no more threads
62 * waiting for the modification.
63 */
64 private final ReentrantLock currentModifierLock = new ReentrantLock();
65 /***
66 * Constructs instance of document modifier.
67 * @param doc modifies this document.
68 * @param views collection of views, opened for this document. This table is
69 * managed, and modified, by XMLAccess, DocumentModifier class doesn't
70 * modify it.
71 * @param listeners listens for document modifications
72 */
73 DocumentModifier(XMLAccess doc, Collection< ? extends DocumentView> views,
74 DocumentListeners listeners) {
75 super();
76 this.doc = doc;
77 this.views = views;
78 this.listeners = listeners;
79 }
80 /***
81 * <p>
82 * Begins modification sequence. All modifications to document must be
83 * enclosed with <code>startModify() .. endModify()</code> sequence. In
84 * other words, modifying document when <code>isModifying()==false</code>
85 * will result in <code>IllegalStateException</code>.
86 * <code>startModify()</code> calls can be nested, i.e. it is possible to
87 * call <code>startModify()</code> when <code>isModifying()</code>, but
88 * every call to <code>startModify()</code> should be closed with
89 * <code>endModify()</code>, otherwise document won't be never
90 * transformed, and other threads will block forever.
91 * </p>
92 * <p>
93 * Two threads cannot modify one document concurrently. If one thread is
94 * currently modifying a document and second thread calls
95 * <code>startModify()</code>, the function will block until current
96 * thread finishes the modification.
97 * </p>
98 */
99 public void startModify() {
100 currentModifierLock.lock();
101
102 }
103 /***
104 * Ends modification of document. It may still be possible to edit document,
105 * when every call to startModify() wasn't yet accompanied by endModify()
106 * call. When last startModify() is closed by this call, transformation is
107 * performed.
108 * @throws ExportException when something goes wrong in the process of
109 * transformation.
110 */
111 public void endModify() throws ExportException {
112 ensureModify();
113 final boolean hasWaitingModifiers = currentModifierLock
114 .hasQueuedThreads();
115 final boolean unlocksLastLock = currentModifierLock.getHoldCount() <= 1;
116
117 currentModifierLock.unlock();
118
119
120
121 if (hasWaitingModifiers)
122 return;
123 if (unlocksLastLock) {
124
125 if (doc.getSplittedChanges().hasChanges()) {
126 for (final DocumentView view : views) {
127 try {
128 view.export();
129 } catch (IOException ex) {
130 throw new ExportException("I/O error: "
131 + ex.getLocalizedMessage(), ex);
132 }
133 }
134 doc.getSplittedChanges().clear();
135 listeners.documentWasTransformed();
136 }
137 }
138 }
139 /***
140 * Returns <code>true</code>, if document is being modified by caller
141 * thread.
142 * @return <code>true</code>, if document is being modified,
143 * <code>false</code> otherwise.
144 */
145 public boolean isModifying() {
146 return currentModifierLock.isHeldByCurrentThread();
147 }
148 /***
149 * Ensures that document is being modified by caller thread. If not,
150 * <code>IllegalStateException</code> is thrown.
151 */
152 void ensureModify() {
153 if (!isModifying())
154 throw new IllegalStateException(
155 "Document is not currently being modified by this thread.");
156 }
157 /***
158 * Sets text of given node to given value. It may be pi, comment, attribute,
159 * text or cdata node only.
160 * @param node the text node, that receives new value.
161 * @param value new text value. <code>null</code> value is equivalent to
162 * an empty string.
163 */
164 public void setText(Node node, String value) {
165 checkReserved(node);
166 DOMMutils.setText(node, value);
167 }
168 /***
169 * Removes given node from the document. If both previous and next sibling
170 * of the node are of same text type (text or cdata) then these nodes are
171 * merged into one.
172 * @param node the node, that will be removed. Its id is removed aswell, but
173 * the id of descendants is not removed.
174 */
175 public void remove(Node node) {
176 checkReserved(node);
177 DOMMutils.remove(node);
178 }
179 /***
180 * Check if such node is reserved and cannot be created in the document.
181 * @param node node to check.
182 * @throws IllegalArgumentException if node is reserved.
183 */
184 private void checkReserved(final Node node) {
185 checkReserved(node.getNamespaceURI(), node.getNodeType());
186 }
187 /***
188 * Check if such node is reserved and cannot be created in the document.
189 * @param namespace the namespace of the node
190 * @param nodeType the type of the node.
191 * @throws IllegalArgumentException if node is reserved.
192 */
193 private void checkReserved(final String namespace, final short nodeType) {
194 if ((nodeType != Node.ATTRIBUTE_NODE)
195 && (nodeType != Node.ELEMENT_NODE))
196 return;
197 if (DOMUtils.equalsURI(namespace, ExportUtils.GENE_ID_ATTRIBUTE_QNAME
198 .getNamespaceURI()))
199 throw new IllegalArgumentException(
200 "Namespace " + ExportUtils.GENE_ID_ATTRIBUTE_QNAME.getNamespaceURI() + " is reserved.");
201 }
202 /***
203 * Creates an attribute. If attribute with given local name and namespace is
204 * already present in context node, its value is modified instead - prefix
205 * is not changed.
206 * @param id id of element, that contains the attribute.
207 * @param prefix prefix of qname of attribute. See
208 * <code>NamespaceManager</code> for details.
209 * @param localName local name of attribute.
210 * @param namespace namespace of attribute.
211 * @param value new value for attribute.
212 * @return ID of newly created attribute.
213 */
214 public String createAttribute(String id, String prefix, String localName,
215 String namespace, String value) {
216 return createAttribute(doc.getIDManager().getElement(id), prefix,
217 localName, namespace, value);
218 }
219 /***
220 * Creates an attribute. If attribute with given local name and namespace is
221 * already present in context node, its value is modified instead - prefix
222 * is not changed.
223 * @param id id of element, that contains the attribute.
224 * @param qname qualified name of the attribute
225 * @param value new value for attribute.
226 * @return ID of newly created attribute.
227 */
228 public String createAttribute(String id, QName qname, String value) {
229 return createAttribute(id, qname.getPrefix(), qname.getLocalPart(),
230 qname.getNamespaceURI(), value);
231 }
232 /***
233 * Creates an attribute. If attribute with given local name and namespace is
234 * already present in context node, its value is modified instead - prefix
235 * is not changed.
236 * @param e element, that contains the attribute.
237 * @param qname qualified name of the attribute
238 * @param value new value for attribute.
239 * @return ID of newly created attribute.
240 */
241 public String createAttribute(Element e, QName qname, String value) {
242 return createAttribute(e, qname.getPrefix(), qname.getLocalPart(),
243 qname.getNamespaceURI(), value);
244 }
245 /***
246 * Creates an attribute. If attribute with given local name and namespace is
247 * already present in context node, its value is modified instead - prefix
248 * is not changed.
249 * @param e element, that contains the attribute.
250 * @param prefix prefix of qname of attribute. See
251 * <code>NamespaceManager</code> for details.
252 * @param localName local name of attribute.
253 * @param namespace namespace of attribute.
254 * @param value new value for attribute.
255 * @return ID of newly created attribute.
256 */
257 public String createAttribute(Element e, String prefix, String localName,
258 String namespace, String value) {
259
260 checkReserved(namespace, Node.ATTRIBUTE_NODE);
261
262 if (DOMUtils.equalsURI(namespace, e.getNamespaceURI())) {
263 namespace = "";
264 prefix = XMLConstants.DEFAULT_NS_PREFIX;
265 }
266 final Attr attr = DOMMutils.createAttribute(e, prefix, localName,
267 namespace, value);
268 return doc.getIDManager().getID(attr);
269 }
270 /***
271 * Replaces the node with another node. <code>newNode</code> must have
272 * <code>null</code> parent or it must be from another document.
273 * @param node the node to be replaced.
274 * @param newNode the node will be replaced with this node. If
275 * <code>null</code> then given node is removed. If not from our document
276 * then it is automatically imported.
277 */
278 public void replaceNode(Node node, Node newNode) {
279 if (newNode == null) {
280 remove(node);
281 return;
282 }
283 doc.checkNode(node);
284 if ((newNode.getParentNode() != null) && (doc.isOurNode(newNode)))
285 throw new IllegalArgumentException(
286 "The replacing node is present in our document");
287 ensureModify();
288
289 final Node nextSibling = node.getNextSibling();
290 final Node parent = node.getParentNode();
291 parent.removeChild(node);
292
293 Node replaceBy = newNode;
294 if (!doc.isOurNode(replaceBy))
295 replaceBy = doc.getDocument().importNode(newNode, true);
296 DOMMutils.insertNode(replaceBy, DomPointerFactory.create(nextSibling),
297 true);
298 }
299 }