001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.xml.text.completion;
043:
044: import java.io.IOException;
045: import java.util.ArrayList;
046: import java.util.Enumeration;
047: import java.util.List;
048: import java.util.LinkedList;
049: import java.util.Collections;
050:
051: import javax.swing.event.DocumentEvent;
052: import javax.swing.event.DocumentListener;
053: import javax.swing.text.BadLocationException;
054: import javax.swing.text.Document;
055: import javax.swing.text.Position;
056: import org.netbeans.api.xml.parsers.DocumentInputSource;
057: import org.netbeans.modules.editor.NbEditorUtilities;
058: import org.netbeans.modules.xml.api.model.ExtendedGrammarQuery;
059: import org.netbeans.modules.xml.api.model.GrammarEnvironment;
060: import org.netbeans.modules.xml.api.model.GrammarQuery;
061: import org.netbeans.modules.xml.api.model.GrammarQueryManager;
062: import org.netbeans.modules.xml.text.syntax.SyntaxElement;
063: import org.netbeans.modules.xml.text.syntax.XMLSyntaxSupport;
064: import org.netbeans.modules.xml.text.syntax.dom.SyntaxNode;
065: import org.openide.filesystems.FileChangeAdapter;
066: import org.openide.filesystems.FileChangeListener;
067: import org.openide.filesystems.FileEvent;
068: import org.openide.filesystems.FileObject;
069: import org.openide.filesystems.FileSystem;
070: import org.openide.loaders.DataObject;
071: import org.openide.text.NbDocument;
072: import org.openide.util.WeakListeners;
073: import org.w3c.dom.Node;
074: import org.xml.sax.InputSource;
075: import org.openide.awt.StatusDisplayer;
076: import org.openide.util.NbBundle;
077:
078: /**
079: * Manages grammar to text editor association. It is able to
080: * dynamically switch among providers.
081: *
082: * @author Petr Kuzel, mfukala@netbeans.org
083: */
084: class GrammarManager extends FileChangeAdapter implements
085: DocumentListener {
086:
087: private static final String FILE_PROTOCOL_URI_PREFIX = "file:/"; //NOI18N
088:
089: private ArrayList<FileObject> externalDTDs = new ArrayList();
090:
091: // current cache state
092: private int state = INVALID;
093:
094: static final int VALID = 1;
095: static final int INVALID = 3;
096:
097: // cache entry
098: private GrammarQuery grammar;
099:
100: // grammar is provided for this document
101: private final XMLSyntaxSupport syntax;
102: private final Document doc;
103:
104: // guarded positions pairs
105: private Position[] guarded;
106:
107: // maximal gurded offset
108: private Position maxGuarded;
109:
110: private int environmentElementsCount = -1;
111:
112: /**
113: * Create new manager.
114: */
115: public GrammarManager(Document doc, XMLSyntaxSupport syntax) {
116: this .doc = doc;
117: this .syntax = syntax;
118: }
119:
120: /**
121: * Return any suitable grammar that you can get
122: * till expires given timeout.
123: */
124: public synchronized GrammarQuery getGrammar() {
125:
126: switch (state) {
127: case VALID:
128: return grammar;
129:
130: case INVALID:
131: loadGrammar();
132: return grammar;
133:
134: default:
135: throw new IllegalStateException();
136: }
137: }
138:
139: /**
140: * Notification from invalidator thread, the grammar need to be reloaded.
141: */
142: public synchronized void invalidateGrammar() {
143:
144: // make current loader a zombie
145: if (state == VALID) {
146: String msg = NbBundle.getMessage(GrammarManager.class,
147: "MSG_loading_cancel");
148: StatusDisplayer.getDefault().setStatusText(msg);
149: }
150:
151: doc.removeDocumentListener(this );
152:
153: //remove FileChangeListeners from the external DTD files
154: for (FileObject fo : externalDTDs) {
155: // System.out.println("[GrammarManager] removed FileObjectListener from " + fo.getPath());
156: fo.removeFileChangeListener(this );
157: }
158: externalDTDs.clear();
159:
160: guarded = new Position[0];
161: state = INVALID;
162: }
163:
164: public void fileChanged(FileEvent fe) {
165: //one of the external DTD files has changed => invalidate grammar
166: invalidateGrammar();
167: }
168:
169: public void fileDeleted(FileEvent fe) {
170: invalidateGrammar();
171: }
172:
173: public void insertUpdate(DocumentEvent e) {
174: //test whether there is a change in the grammar environment - e.g. is a grammar
175: //declaration was added and so.
176: checkDocumentEnvironment(e);
177:
178: if (isGuarded(e.getOffset(), e.getLength())) {
179: invalidateGrammar();
180: }
181: }
182:
183: public void removeUpdate(DocumentEvent e) {
184: //test whether there is a change in the grammar environment - e.g. is a grammar
185: //declaration was removed and so.
186: checkDocumentEnvironment(e);
187:
188: if (isGuarded(e.getOffset(), e.getLength())) {
189: invalidateGrammar();
190: }
191: }
192:
193: private void checkDocumentEnvironment(DocumentEvent e) {
194: long current = System.currentTimeMillis();
195:
196: try {
197: LinkedList ll = getEnvironmentElements();
198: if (ll.size() != environmentElementsCount) {
199: invalidateGrammar();
200: environmentElementsCount = ll.size();
201: }
202: } catch (BadLocationException ble) {
203: }
204:
205: }
206:
207: public void changedUpdate(DocumentEvent e) {
208: // not interested
209: }
210:
211: private boolean isGuarded(int offset, int length) {
212:
213: // optimalization for common case
214: if ((maxGuarded != null) && (offset > maxGuarded.getOffset())) {
215: return false;
216: }
217:
218: if (guarded == null) {
219: return false;
220: }
221:
222: // slow loop matchibng range overlaps
223: for (int i = 0; i < guarded.length; i += 2) {
224: int start = guarded[i].getOffset();
225: int end = guarded[i + 1].getOffset();
226: if (start < offset && offset < end) {
227: return true;
228: }
229: int changeEnd = offset + length;
230: if (offset < start && start < changeEnd) {
231: return true;
232: }
233: }
234:
235: return false;
236: }
237:
238: /**
239: * Nofification from grammar loader thread, new valid grammar.
240: * @param grammar grammar or <code>null</code> if cannot load.
241: */
242: private synchronized void grammarLoaded(GrammarQuery grammar) {
243: //Issue: http://www.netbeans.org/issues/show_bug.cgi?id=108610
244: //do not show error message since there can be many more completion providers.
245: String status = (grammar != null) ? NbBundle.getMessage(
246: GrammarManager.class, "MSG_loading_done") : null;
247:
248: this .grammar = grammar == null ? EmptyQuery.INSTANCE : grammar;
249: state = VALID;
250:
251: StatusDisplayer.getDefault().setStatusText(status);
252: }
253:
254: /**
255: * Async grammar fetching
256: */
257: private void loadGrammar() {
258:
259: GrammarQuery loaded = null;
260: try {
261:
262: String status = NbBundle.getMessage(GrammarManager.class,
263: "MSG_loading");
264: StatusDisplayer.getDefault().setStatusText(status);
265:
266: // prepare grammar environment
267:
268: try {
269:
270: LinkedList ctx = getEnvironmentElements();
271: InputSource inputSource = new DocumentInputSource(doc);
272: FileObject fileObject = null;
273: Object obj = doc
274: .getProperty(Document.StreamDescriptionProperty);
275: if (obj instanceof DataObject) {
276: DataObject dobj = (DataObject) obj;
277: fileObject = dobj.getPrimaryFile();
278: }
279: GrammarEnvironment env = new GrammarEnvironment(
280: Collections.enumeration(ctx), inputSource,
281: fileObject);
282:
283: // lookup for grammar
284:
285: GrammarQueryManager g = GrammarQueryManager
286: .getDefault();
287: Enumeration en = g.enabled(env);
288: if (en == null)
289: return;
290:
291: // set guarded regions
292:
293: List positions = new ArrayList(10);
294: int max = 0;
295:
296: while (en.hasMoreElements()) {
297: Node next = (Node) en.nextElement();
298: if (next instanceof SyntaxNode) {
299: SyntaxNode node = (SyntaxNode) next;
300: int start = node.getElementOffset();
301: int end = start + node.getElementLength();
302: if (end > max)
303: max = end;
304: Position startPosition = NbDocument
305: .createPosition(doc, start,
306: Position.Bias.Forward);
307: positions.add(startPosition);
308: Position endPosition = NbDocument
309: .createPosition(doc, end,
310: Position.Bias.Backward);
311: positions.add(endPosition);
312: }
313: }
314:
315: guarded = (Position[]) positions
316: .toArray(new Position[positions.size()]);
317: maxGuarded = NbDocument.createPosition(doc, max,
318: Position.Bias.Backward);
319:
320: // retrieve the grammar and start invalidation listener
321:
322: loaded = g.getGrammar(env);
323:
324: if (loaded instanceof ExtendedGrammarQuery) {
325: //attach listeners to external files and if any of them changes then reload this grammar
326: for (String resolvedEntity : (List<String>) ((ExtendedGrammarQuery) loaded)
327: .getResolvedEntities()) {
328: //filter non-files resolved entities
329: if (!resolvedEntity
330: .startsWith(FILE_PROTOCOL_URI_PREFIX))
331: continue;
332:
333: DataObject docDo = NbEditorUtilities
334: .getDataObject(doc);
335: if (docDo != null) {
336: FileObject docFo = docDo.getPrimaryFile();
337: if (docFo != null) {
338: try {
339: FileSystem fs = docFo
340: .getFileSystem();
341: FileObject fo = fs
342: .findResource(resolvedEntity
343: .substring(FILE_PROTOCOL_URI_PREFIX
344: .length())); //NOI18N
345: if (fo != null) {
346: externalDTDs.add(fo);
347: //add a week listener to the fileobject - detach when document is being disposed
348: fo
349: .addFileChangeListener((FileChangeListener) WeakListeners
350: .create(
351: FileChangeListener.class,
352: this ,
353: doc));
354: // System.out.println("[GrammarManager] added FileObjectListener to " + fo.getPath());
355: }
356: } catch (IOException e) {
357: e.printStackTrace();
358: }
359: }
360: }
361: }
362: }
363:
364: } catch (BadLocationException ex) {
365: loaded = null;
366: }
367:
368: } finally {
369:
370: doc.addDocumentListener(GrammarManager.this );
371:
372: grammarLoaded(loaded);
373: }
374: }
375:
376: private LinkedList getEnvironmentElements()
377: throws BadLocationException {
378: LinkedList ctx = new LinkedList();
379: SyntaxElement first = syntax.getElementChain(1);
380: while (true) {
381: if (first == null)
382: break;
383: if (first instanceof SyntaxNode) {
384: SyntaxNode node = (SyntaxNode) first;
385: ctx.add(node);
386: if (node.ELEMENT_NODE == node.getNodeType()) {
387: break;
388: }
389: }
390: first = first.getNext();
391: }
392: return ctx;
393: }
394: }
|