View Javadoc

1   /*
2    * Copyright 1999-2006 Faculty of Mathematics, Physics and Informatics, Comenius
3    * University, Bratislava. This file is protected by the Mozilla Public License
4    * version 1.1 (the "License"); you may not use this file except in compliance
5    * with the License. You may obtain a copy of the License at
6    * http://euromath2.sourceforge.net/license.html Unless required by applicable
7    * law or agreed to in writing, software distributed under the License is
8    * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
9    * KIND, either express or implied. See the License for the specific language
10   * governing permissions and limitations under the License.
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 		// this thread granted the right to modify the document.
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 		// unlock the lock.
117 		currentModifierLock.unlock();
118 		// if more modifiers are waiting for the modification to start, quit
119 		// immediately and let another thread execute startModify and gain the
120 		// lock.
121 		if (hasWaitingModifiers)
122 			return;
123 		if (unlocksLastLock) {
124 			// if something changed then execute the transformation
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: " //$NON-NLS-1$
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."); //$NON-NLS-1$
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."); //$NON-NLS-1$ //$NON-NLS-2$
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 		// forbid creating reserved attribute
260 		checkReserved(namespace, Node.ATTRIBUTE_NODE);
261 		// if namespace is equal to namespace of parent element, then null it.
262 		if (DOMUtils.equalsURI(namespace, e.getNamespaceURI())) {
263 			namespace = ""; //$NON-NLS-1$
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"); //$NON-NLS-1$
287 		ensureModify();
288 		// remove old node from the document
289 		final Node nextSibling = node.getNextSibling();
290 		final Node parent = node.getParentNode();
291 		parent.removeChild(node);
292 		// insert new node into its place
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 }