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.util.*;
045: import java.util.Enumeration;
046: import javax.swing.text.JTextComponent;
047: import javax.swing.text.BadLocationException;
048: import javax.swing.text.Document;
049: import org.netbeans.modules.xml.text.api.XMLDefaultTokenContext;
050:
051: import org.w3c.dom.*;
052:
053: import org.netbeans.editor.*;
054: import org.netbeans.editor.ext.*;
055: import org.openide.ErrorManager;
056:
057: import org.netbeans.modules.xml.text.syntax.*;
058: import org.netbeans.modules.xml.text.syntax.dom.*;
059: import org.netbeans.modules.xml.api.model.*;
060: import org.netbeans.modules.xml.spi.dom.UOException;
061: import org.netbeans.modules.xml.text.syntax.dom.SyntaxNode;
062: import org.openide.util.NbBundle;
063:
064: /**
065: * Consults grammar and presents list of possible choices
066: * in particular document context.
067: * <p>
068: * <b>Warning:</b> It is public for unit test purposes only!
069: *
070: * @author Petr Nejedly
071: * @author Sandeep Randhawa
072: * @author Petr Kuzel
073: * @author asgeir@dimonsoftware.com
074: * @version 1.01
075: */
076:
077: public class XMLCompletionQuery implements CompletionQuery, XMLTokenIDs {
078:
079: // the name of a property indentifing cached query
080: public static final String DOCUMENT_GRAMMAR_BINDING_PROP = "doc-bind-query";
081:
082: /**
083: * Perform the query on the given component. The query usually
084: * gets the component's document, the caret position and searches back
085: * to examine surrounding context. Then it returns the result.
086: * <p>
087: * It is also called after every keystroke while opened completion
088: * popup. So some result cache could be used. It is not easy at
089: * this level because of BACKSPACE that can extend result.
090: *
091: * @param component the component to use in this query.
092: * @param offset position in the component's document to which the query will
093: * be performed. Usually it's a caret position.
094: * @param support syntax-support that will be used during resolving of the query.
095: * @return result of the query or null if there's no result.
096: */
097: public CompletionQuery.Result query(JTextComponent component,
098: int offset, SyntaxSupport support) {
099:
100: // assert precondition, actually serial access required
101:
102: // synchronized (this) {
103: // if (thread == null) {
104: // thread = new ThreadLocal();
105: // thread.set(thread);
106: // }
107: // if (thread != thread.get()) {
108: // // unfortunatelly RP can probably provide serialization even
109: // // if delegating to multiple threads
110: // // throw new IllegalStateException("Serial access required!"); //NOI18N
111: // }
112: // }
113:
114: // perform query
115:
116: BaseDocument doc = (BaseDocument) component.getDocument();
117: if (doc == null)
118: return null;
119: XMLSyntaxSupport sup = (XMLSyntaxSupport) support
120: .get(XMLSyntaxSupport.class);
121: if (sup == null)
122: return null;// No SyntaxSupport for us, no hint for user
123:
124: try {
125: SyntaxQueryHelper helper = new SyntaxQueryHelper(sup,
126: offset);
127:
128: //supposing that there wont be more cc items result sets in one moment hence
129: //using the static field, otherwise the substitute offset would have to be set to all CC items
130: XMLResultItem.substituteOffset = helper.getOffset()
131: - helper.getEraseCount();
132:
133: // completion request originates from area covered by DOM,
134: if (helper.getCompletionType() != SyntaxQueryHelper.COMPLETION_TYPE_DTD) {
135: List all = new ArrayList();
136: List list = null;
137: switch (helper.getCompletionType()) {
138: case SyntaxQueryHelper.COMPLETION_TYPE_ATTRIBUTE:
139: list = queryAttributes(helper, doc, sup);
140: break;
141: case SyntaxQueryHelper.COMPLETION_TYPE_VALUE:
142: list = queryValues(helper, doc, sup);
143: break;
144: case SyntaxQueryHelper.COMPLETION_TYPE_ELEMENT:
145: list = queryElements(helper, doc, sup);
146: break;
147: case SyntaxQueryHelper.COMPLETION_TYPE_ENTITY:
148: list = queryEntities(helper, doc, sup);
149: break;
150: case SyntaxQueryHelper.COMPLETION_TYPE_NOTATION:
151: list = queryNotations(helper, doc, sup);
152: case SyntaxQueryHelper.COMPLETION_TYPE_UNKNOWN:
153: return null; //do not show the CC
154: }
155:
156: if (list == null) {
157: // broken document
158: return cannotSuggest(component, sup
159: .requestedAutoCompletion());
160: }
161:
162: if (helper.getCompletionType() == SyntaxQueryHelper.COMPLETION_TYPE_VALUE) {
163: //might be the end tag autocompletion
164: if (helper.getToken().getTokenID() == XMLDefaultTokenContext.TAG) {
165: SyntaxElement se = helper.getSyntaxElement();
166: if (se instanceof StartTag) {
167: String tagName = ((StartTag) se)
168: .getNodeName();
169: if (tagName != null)
170: list
171: .add(new EndTagAutocompletionResultItem(
172: tagName));
173: }
174: }
175: }
176:
177: if (list.isEmpty()
178: && helper.getPreText().startsWith("</")) { // NOI18N
179: List stlist = findStartTag((SyntaxNode) helper
180: .getSyntaxElement(), !helper.getPreText()
181: .endsWith("/") ? "/" : "");
182: if (stlist != null && !stlist.isEmpty()) {
183: ElementResultItem item = (ElementResultItem) stlist
184: .get(0); //we always get just one item
185: if (!item.getItemText().startsWith("/")
186: || item.getItemText().startsWith(
187: helper.getPreText()
188: .substring(1))) {
189: String title = NbBundle.getMessage(
190: XMLCompletionQuery.class,
191: "MSG_result", helper.getPreText());
192: return new XMLCompletionResult(component,
193: title, stlist, helper.getOffset(),
194: 0);
195: }
196: }
197: }
198:
199: String debugMsg = Boolean
200: .getBoolean("netbeans.debug.xml") ? " "
201: + helper.getOffset() + "-"
202: + helper.getEraseCount() : "";
203: String title = NbBundle.getMessage(
204: XMLCompletionQuery.class, "MSG_result", helper
205: .getPreText())
206: + debugMsg;
207:
208: // add to the list end tag if detected '<'
209: // unless following end tag is of the same name
210:
211: if (helper.getPreText().endsWith("<")
212: && helper.getToken().getTokenID() == TEXT) { // NOI18N
213: List startTags = findStartTag((SyntaxNode) helper
214: .getSyntaxElement(), "/"); // NOI18N
215:
216: boolean addEndTag = true;
217: SyntaxNode ctx = (SyntaxNode) helper
218: .getSyntaxElement();
219: SyntaxElement nextElement = ctx != null ? ctx
220: .getNext() : null;
221: if (nextElement instanceof EndTag) {
222: EndTag endtag = (EndTag) nextElement;
223: String nodename = endtag.getNodeName();
224: if (nodename != null
225: && startTags.isEmpty() == false) {
226: ElementResultItem item = (ElementResultItem) startTags
227: .get(0);
228: if (("/" + nodename).equals(item
229: .getItemText())) { // NOI18N
230: addEndTag = false;
231: }
232: }
233: }
234:
235: if (addEndTag) {
236: //prevent autocompletion of the only CC item.
237: if (!list.isEmpty() || startTags.size() > 1) {
238: all.addAll(startTags);
239: }
240: }
241:
242: }
243:
244: all.addAll(list);
245:
246: if (all.isEmpty()) {
247: return noSuggestion(component, sup
248: .requestedAutoCompletion());
249: } else {
250: return new XMLCompletionResult(component, title,
251: all, helper.getOffset()
252: - helper.getEraseCount(), helper
253: .getEraseCount());
254: }
255:
256: } else {
257: // prolog, internal DTD no completition yet
258: if (helper.getToken().getTokenID() == PI_CONTENT) {
259: if (helper.getPreText().endsWith("encoding=")) { // NOI18N
260: List encodings = new ArrayList(2);
261: encodings.add(new XMLResultItem("\"UTF-8\"")); // NOI18N
262: encodings.add(new XMLResultItem("\"UTF-16\"")); // NOI18N
263: return new XMLCompletionResult(component,
264: NbBundle.getMessage(
265: XMLCompletionQuery.class,
266: "MSG_encoding_comp"),
267: encodings, helper.getOffset(), 0);
268: }
269: }
270: return noSuggestion(component, sup
271: .requestedAutoCompletion());
272: }
273:
274: } catch (BadLocationException e) {
275: //Util.THIS.debug(e);
276: }
277:
278: // nobody knows what happened...
279: return noSuggestion(component, sup.requestedAutoCompletion());
280: }
281:
282: /**
283: * Contruct result indicating that grammar is not able to give
284: * a hint because document is too broken or invalid. Grammar
285: * knows that it's broken.
286: */
287: private static Result cannotSuggest(JTextComponent component,
288: boolean auto) {
289: if (auto)
290: return null;
291: return new XMLCompletionResult(component, NbBundle.getMessage(
292: XMLCompletionQuery.class, "BK0002"),
293: Collections.EMPTY_LIST, 0, 0);
294: }
295:
296: /**
297: * Contruct result indicating that grammar is not able to give
298: * a hint because in given context is not nothing allowed what
299: * the grammar know of. May grammar is missing at all.
300: */
301: private static Result noSuggestion(JTextComponent component,
302: boolean auto) {
303: if (auto)
304: return null;
305: return new XMLCompletionResult(component, NbBundle.getMessage(
306: XMLCompletionQuery.class, "BK0003"),
307: Collections.EMPTY_LIST, 0, 0);
308: }
309:
310: // Grammar binding ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
311:
312: /**
313: * Obtain grammar manager, cache results in document property
314: * <code>PROP_DOCUMENT_QUERY</code>. It is always called from single
315: * thread.
316: */
317: public static GrammarQuery getPerformer(Document doc,
318: XMLSyntaxSupport sup) {
319:
320: Object grammarBindingObj = doc
321: .getProperty(DOCUMENT_GRAMMAR_BINDING_PROP);
322:
323: if (grammarBindingObj == null) {
324: grammarBindingObj = new GrammarManager(doc, sup);
325: doc.putProperty(DOCUMENT_GRAMMAR_BINDING_PROP,
326: grammarBindingObj);
327: }
328:
329: return ((GrammarManager) grammarBindingObj).getGrammar();
330: }
331:
332: // Delegate queriing to performer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
333:
334: private List queryEntities(SyntaxQueryHelper helper, Document doc,
335: XMLSyntaxSupport sup) {
336: Enumeration res = getPerformer(doc, sup).queryEntities(
337: helper.getContext().getCurrentPrefix());
338: return translateEntityRefs(res);
339: }
340:
341: private List queryElements(SyntaxQueryHelper helper, Document doc,
342: XMLSyntaxSupport sup) {
343: try {
344: GrammarQuery performer = getPerformer(doc, sup);
345: HintContext ctx = helper.getContext();
346: //66607 hacky fix - see the issue comment
347: if (helper.getPreText().startsWith("</")) {
348: return Collections.EMPTY_LIST;
349: }
350: String typedPrefix = ctx.getCurrentPrefix();
351: Enumeration res = performer.queryElements(ctx);
352: return translateElements(res, typedPrefix, performer);
353: } catch (UOException e) {
354: ErrorManager.getDefault().notify(e);
355: return null;
356: }
357: }
358:
359: private List queryAttributes(SyntaxQueryHelper helper,
360: Document doc, XMLSyntaxSupport sup) {
361: Enumeration res = getPerformer(doc, sup).queryAttributes(
362: helper.getContext());
363: return translateAttributes(res, helper.isBoundary());
364: }
365:
366: private List queryValues(SyntaxQueryHelper helper, Document doc,
367: XMLSyntaxSupport sup) {
368: Enumeration res = getPerformer(doc, sup).queryValues(
369: helper.getContext());
370: return translateValues(res);
371: }
372:
373: private List queryNotations(SyntaxQueryHelper helper, Document doc,
374: XMLSyntaxSupport sup) { //!!! to be implemented
375: Enumeration res = getPerformer(doc, sup).queryNotations(
376: helper.getContext().getCurrentPrefix());
377: return null;
378: }
379:
380: // Translate general results to editor ones ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
381:
382: private List translateEntityRefs(Enumeration refs) {
383: List result = new ArrayList(133);
384: while (refs.hasMoreElements()) {
385: GrammarResult next = (GrammarResult) refs.nextElement();
386: if (next != null && next.getNodeName() != null) {
387: EntityRefResultItem ref = new EntityRefResultItem(next);
388: result.add(ref);
389: }
390: }
391: return result;
392: }
393:
394: /** Translate results perfromer (DOM nodes) format to CompletionQuery.ResultItems format. */
395: private List translateElements(Enumeration els, String prefix,
396: GrammarQuery perfomer) {
397: List result = new ArrayList(13);
398: while (els.hasMoreElements()) {
399: GrammarResult next = (GrammarResult) els.nextElement();
400: if (prefix.equals(next.getNodeName())) {
401: // XXX It's probably OK that perfomer has returned it, we just do not want to visualize it
402: // ErrorManager err =ErrorManager.getDefault();
403: // err.log(ErrorManager.WARNING, "Grammar " + perfomer.getClass().getName() + " result '" + prefix + "' eliminated to avoid #28224."); // NOi18N
404: continue;
405: }
406: if (next != null && next.getNodeName() != null) {
407: ElementResultItem ei = new ElementResultItem(next);
408: result.add(ei);
409: }
410: }
411: return result;
412: }
413:
414: private List translateAttributes(Enumeration attrs, boolean boundary) {
415: List result = new ArrayList(13);
416: while (attrs.hasMoreElements()) {
417: GrammarResult next = (GrammarResult) attrs.nextElement();
418: if (next != null && next.getNodeName() != null) {
419: AttributeResultItem attr = new AttributeResultItem(
420: next, false);
421: result.add(attr);
422: }
423: }
424: return result;
425: }
426:
427: private List translateValues(Enumeration values) {
428: List result = new ArrayList(3);
429: while (values.hasMoreElements()) {
430: GrammarResult next = (GrammarResult) values.nextElement();
431: if (next != null && next.getDisplayName() != null) {
432: ValueResultItem val = new ValueResultItem(next);
433: result.add(val);
434: }
435: }
436: return result;
437: }
438:
439: /**
440: * User just typed <sample></</sample> so we must locate
441: * paing start tag.
442: * @param text pointer to starting context
443: * @param prefix that is prepended to created ElementResult e.g. '</'
444: * @return list with one ElementResult or empty.
445: */
446: private static List findStartTag(SyntaxNode text, String prefix) {
447: //if ( Util.THIS.isLoggable() ) /* then */ Util.THIS.debug("XMLCompletionQuery.findStartTag: text=" + text);
448:
449: Node parent = text.getParentNode();
450: if (parent == null) {
451: return Collections.EMPTY_LIST;
452: }
453:
454: String name = parent.getNodeName();
455: //if ( Util.THIS.isLoggable() ) Util.THIS.debug(" name=" + name);
456: if (name == null) {
457: return Collections.EMPTY_LIST;
458: }
459:
460: XMLResultItem res = new ElementResultItem(prefix + name);
461: //if ( Util.THIS.isLoggable() ) Util.THIS.debug(" result=" + res);
462:
463: List list = new ArrayList(1);
464: list.add(res);
465:
466: return list;
467: }
468:
469: private static List findStartTag(SyntaxNode text) {
470: return findStartTag(text, "");
471: }
472:
473: public static class XMLCompletionResult extends
474: CompletionQuery.DefaultResult {
475: private int substituteOffset;
476:
477: public XMLCompletionResult(JTextComponent component,
478: String title, List data, int offset, int len) {
479: super (component, title, data, offset, len);
480: substituteOffset = offset;
481: }
482:
483: public int getSubstituteOffset() {
484: return substituteOffset;
485: }
486: }
487:
488: }
|