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.structure;
043:
044: import java.util.ArrayList;
045: import java.util.Collection;
046: import java.util.Collections;
047: import java.util.HashMap;
048: import java.util.Iterator;
049: import java.util.List;
050: import java.util.Map;
051: import java.util.Set;
052: import java.util.Stack;
053: import java.util.TreeSet;
054: import javax.swing.text.AttributeSet;
055: import javax.swing.text.BadLocationException;
056: import org.netbeans.editor.BaseDocument;
057: import org.netbeans.editor.TokenItem;
058: import org.netbeans.modules.editor.structure.api.DocumentElement;
059: import org.netbeans.modules.editor.structure.api.DocumentModel;
060: import org.netbeans.modules.editor.structure.api.DocumentModel.DocumentChange;
061: import org.netbeans.modules.editor.structure.api.DocumentModel.DocumentModelTransactionCancelledException;
062: import org.netbeans.modules.editor.structure.api.DocumentModelException;
063: import org.netbeans.modules.editor.structure.api.DocumentModelUtils;
064: import org.netbeans.modules.editor.structure.spi.DocumentModelProvider;
065: import org.netbeans.modules.xml.text.syntax.SyntaxElement;
066: import org.netbeans.modules.xml.text.syntax.XMLSyntaxSupport;
067: import org.netbeans.modules.xml.text.syntax.XMLTokenIDs;
068: import org.netbeans.modules.xml.text.syntax.dom.AttrImpl;
069: import org.netbeans.modules.xml.text.syntax.dom.CDATASectionImpl;
070: import org.netbeans.modules.xml.text.syntax.dom.CommentImpl;
071: import org.netbeans.modules.xml.text.syntax.dom.DocumentTypeImpl;
072: import org.netbeans.modules.xml.text.syntax.dom.EmptyTag;
073: import org.netbeans.modules.xml.text.syntax.dom.EndTag;
074: import org.netbeans.modules.xml.text.syntax.dom.ProcessingInstructionImpl;
075: import org.netbeans.modules.xml.text.syntax.dom.StartTag;
076: import org.netbeans.modules.xml.text.syntax.dom.Tag;
077: import org.openide.ErrorManager;
078:
079: /**
080: *
081: * @author mf100882
082: */
083: public class XMLDocumentModelProvider implements DocumentModelProvider {
084:
085: public void updateModel(
086: DocumentModel.DocumentModelModificationTransaction dtm,
087: DocumentModel model, DocumentChange[] changes)
088: throws DocumentModelException,
089: DocumentModelTransactionCancelledException {
090:
091: long a = System.currentTimeMillis();
092:
093: if (debug)
094: System.out.println("\n\n\n\n\n");
095: if (debug)
096: DocumentModelUtils.dumpElementStructure(model
097: .getRootElement());
098:
099: ArrayList regenerate = new ArrayList(); //used to store elements to be regenerated
100:
101: for (int i = 0; i < changes.length; i++) {
102: DocumentChange dch = changes[i];
103:
104: int changeOffset = dch.getChangeStart().getOffset();
105: int changeLength = dch.getChangeLength();
106:
107: //find an element in which the change happened
108: DocumentElement leaf = model
109: .getLeafElementForOffset(changeOffset);
110: DocumentElement toRegenerate = leaf;
111:
112: if (debug)
113: System.out.println("");
114: if (debug)
115: System.out.println(dch);
116: try {
117: if (debug)
118: System.out.println("inserted text:'"
119: + model.getDocument().getText(changeOffset,
120: changeLength) + "'");
121: } catch (BadLocationException e) {
122: ;
123: }
124: if (debug)
125: System.out.println("leaf = " + leaf);
126:
127: //parse the document context
128: XMLSyntaxSupport sup = (XMLSyntaxSupport) ((BaseDocument) model
129: .getDocument()).getSyntaxSupport();
130:
131: boolean textOnly = false;
132: boolean attribsOnly = false;
133: try {
134: //scan the inserted text - if it contains only text set textOnly flag
135: TokenItem ti = sup.getTokenChain(changeOffset,
136: changeOffset + 1);
137: while (ti != null
138: && ti.getOffset() < (changeOffset + changeLength)) {
139: if (ti.getTokenID() == XMLTokenIDs.TEXT
140: || ti.getTokenID() == XMLTokenIDs.DECLARATION
141: || ti.getTokenID() == XMLTokenIDs.BLOCK_COMMENT
142: || ti.getTokenID() == XMLTokenIDs.PI_CONTENT
143: || ti.getTokenID() == XMLTokenIDs.CDATA_SECTION) {
144: textOnly = true;
145: break;
146: }
147: if (ti.getTokenID() == XMLTokenIDs.ARGUMENT
148: || ti.getTokenID() == XMLTokenIDs.OPERATOR
149: || ti.getTokenID() == XMLTokenIDs.VALUE) {
150: attribsOnly = true;
151: break;
152: }
153: ti = ti.getNext();
154: }
155: } catch (BadLocationException e) {
156: ErrorManager.getDefault().notify(ErrorManager.WARNING,
157: e);
158: }
159:
160: if (textOnly
161: && (leaf.getType().equals(XML_CONTENT)
162: || leaf.getType().equals(XML_DOCTYPE)
163: || leaf.getType().equals(XML_PI)
164: || leaf.getType().equals(XML_COMMENT) || leaf
165: .getType().equals(XML_CDATA))) {
166: //just a text written into a text element simply fire document element change event and do not regenerate anything
167: //add the element update request into transaction
168: if (debug)
169: System.out.println("ONLY CONTENT UPDATE!!!");
170: dtm.updateDocumentElementText(leaf);
171:
172: //do not scan the context tag if the change is only insert or remove of one character into a text (typing text perf. optimalization)
173: if (dch.getChangeLength() == 1) {
174: continue;
175: }
176: }
177:
178: if ((attribsOnly || dch.getChangeType() == DocumentChange.REMOVE)
179: && (leaf.getType().equals(XML_TAG) || leaf
180: .getType().equals(XML_EMPTY_TAG))) {
181: if (debug)
182: System.out.println("POSSIBLE ATTRIBS UPDATE!!!");
183: //we need to parse the tag element attributes and set them according to the new values
184: try {
185: SyntaxElement sel = sup.getElementChain(leaf
186: .getStartOffset() + 1);
187: if (sel instanceof Tag || sel instanceof EmptyTag) {
188: //test whether the attributes changed
189: Map newAttrs = createAttributesMap((Tag) sel);
190: AttributeSet existing = leaf.getAttributes();
191: boolean update = false;
192: if (existing.getAttributeCount() == newAttrs
193: .size()) {
194: Iterator itr = newAttrs.keySet().iterator();
195: while (itr.hasNext()) {
196: String attrName = (String) itr.next();
197: String attrValue = (String) newAttrs
198: .get(attrName);
199: if (attrName != null
200: && attrValue != null
201: && !existing.containsAttribute(
202: attrName, attrValue)) {
203: update = true;
204: break;
205: }
206:
207: }
208: } else
209: update = true;
210:
211: if (update) {
212: dtm.updateDocumentElementAttribs(leaf,
213: newAttrs);
214: }
215: }
216: } catch (BadLocationException ble) {
217: ErrorManager.getDefault().notify(
218: ErrorManager.WARNING, ble);
219: }
220: }
221:
222: //if one or more elements are deleted get correct paret to regenerate
223: if ((leaf.getStartOffset() == leaf.getEndOffset())
224: || (changeOffset == leaf.getStartOffset())
225: || (changeOffset == leaf.getEndOffset()))
226: toRegenerate = leaf.getParentElement();
227: else {
228: //user written a tag or something what is not a text
229: //we need to get the element's parent. Simple leaf.getParent() is not enought
230: //since when an element is deleted then a wrong parent can be choosen
231: if (leaf.getType().equals(XML_CONTENT)) {
232: do {
233: toRegenerate = toRegenerate.getParentElement();
234: } while (toRegenerate != null
235: && toRegenerate.getType().equals(
236: XML_CONTENT));
237:
238: if (toRegenerate == null) {
239: //no suitable parent found - the element is either a root or doesn't have any xml_tag ancestor => use root
240: toRegenerate = model.getRootElement();
241: }
242: }
243: }
244:
245: if (toRegenerate == null)
246: toRegenerate = model.getRootElement(); //root element is empty
247:
248: //now regenerate all sub-elements inside parent of the affected element
249:
250: //check if the element is not a descendant a one of the elements
251: //which are going to be regenerated
252: Iterator itr = regenerate.iterator();
253: boolean hasAncestor = false;
254: while (itr.hasNext()) {
255: DocumentElement de = (DocumentElement) itr.next();
256: if (de.equals(toRegenerate)
257: || model.isDescendantOf(de, toRegenerate)) {
258: hasAncestor = true;
259: break;
260: }
261: }
262:
263: if (!hasAncestor) {
264: //check whether the element is not an ancestor of one or more element
265: //which are going to be regenerated
266: ArrayList toRemove = new ArrayList();
267: Iterator i2 = regenerate.iterator();
268: while (i2.hasNext()) {
269: DocumentElement de = (DocumentElement) i2.next();
270: if (model.isDescendantOf(toRegenerate, de))
271: toRemove.add(de);
272: }
273:
274: //now really remove the elements
275: regenerate.removeAll(toRemove);
276:
277: //add the element - it will be likely regenerated in next model update
278: regenerate.add(toRegenerate);
279:
280: //debug>>>
281: if (debug)
282: System.out
283: .println("===================================================================");
284: if (debug)
285: System.out.println("change happened in " + leaf);
286: if (debug)
287: System.out.println("we will regenerate its parent "
288: + toRegenerate);
289: //<<<debug
290: }
291: } //end of the changes loop
292:
293: //update the model
294: Iterator elementsToUpdate = regenerate.iterator();
295: while (elementsToUpdate.hasNext()) {
296: DocumentElement de = (DocumentElement) elementsToUpdate
297: .next();
298: generateDocumentElements(dtm, model, de);
299: }
300:
301: if (measure)
302: System.out.println("[xmlmodel] generated in "
303: + (System.currentTimeMillis() - a));
304:
305: }
306:
307: /** generates document elements within an area defined by startoffset and
308: *endoffset. */
309: private void generateDocumentElements(
310: DocumentModel.DocumentModelModificationTransaction dtm,
311: DocumentModel model, DocumentElement de)
312: throws DocumentModelException,
313: DocumentModelTransactionCancelledException {
314:
315: int startOffset = de.getStartOffset();
316: int endOffset = de.getEndOffset();
317:
318: BaseDocument doc = (BaseDocument) model.getDocument();
319: XMLSyntaxSupport sup = new XMLSyntaxSupport(doc);
320:
321: if (debug)
322: System.out
323: .println("[XMLDocumentModelProvider] regenerating "
324: + de);
325:
326: Set addedElements = new TreeSet(
327: DocumentModel.ELEMENTS_COMPARATOR);
328: ArrayList skipped = new ArrayList();
329: try {
330: Stack elementsStack = new Stack(); //we need this to determine tags nesting
331:
332: //the syntax element is created for token on offset - 1
333: //so I need to add 1 to the startOffset
334: SyntaxElement sel = sup.getElementChain(Math.min(doc
335: .getLength(), startOffset + 1));
336:
337: //scan the document for syntax elements - from startOffset to endOffset
338: while (sel != null
339: && getSyntaxElementEndOffset(sel) <= endOffset) {
340: if (sel instanceof SyntaxElement.Error) {
341: //add error element into the structure
342: if (debug)
343: System.out
344: .println("Error found! => adding error element.");
345: String errorText = doc
346: .getText(sel.getElementOffset(), sel
347: .getElementLength());
348: addedElements.add(dtm.addDocumentElement(errorText,
349: XML_ERROR, Collections.EMPTY_MAP, sel
350: .getElementOffset(),
351: getSyntaxElementEndOffset(sel)));
352: }
353:
354: if (sel instanceof StartTag) {
355: //test if there is already an existing documet element in the model
356: StartTag stag = (StartTag) sel;
357: DocumentElement tagDE = DocumentModelUtils
358: .findElement(model, sel.getElementOffset(),
359: stag.getTagName(), XML_TAG);
360:
361: //do not skip the 'de' element which is to be regenerated
362: if (tagDE != null && !tagDE.equals(de)) {
363: //test if the element has also correct end tag
364: SyntaxElement endTagCheck = sup
365: .getElementChain(Math.min(doc
366: .getLength(), tagDE
367: .getEndOffset() + 1));
368: if (endTagCheck instanceof EndTag
369: && ((EndTag) endTagCheck).getTagName()
370: .equals(stag.getTagName())) {
371: //there is an element - skip it - analyze an element after the end of the
372: //existing element
373: if (debug)
374: System.out
375: .println("found existing element "
376: + tagDE
377: + " => skipping");
378: //sel = sup.getElementChain(Math.min(doc.getLength(), tagDE.getEndOffset() + 2));
379: sel = endTagCheck.getNext();
380: skipped.add(tagDE);
381: continue;
382: }
383: }
384:
385: //add the tag syntax element into stack
386: elementsStack.push(sel);
387:
388: } else if (sel instanceof EndTag) {
389: if (!elementsStack.isEmpty()) {
390: StartTag latest = (StartTag) elementsStack
391: .peek();
392: if (((EndTag) sel).getTagName().equals(
393: latest.getTagName())) {
394: //we have encountered a pair end tag to open tag on the peek of the stack
395: Map attribs = createAttributesMap(latest);
396: addedElements.add(dtm.addDocumentElement(
397: latest.getTagName(), XML_TAG,
398: attribs, latest.getElementOffset(),
399: getSyntaxElementEndOffset(sel)));
400: //remove the open tag syntax element from the stack
401: elementsStack.pop();
402: } else {
403: //found an end tag which doesn't have a start tag
404: //=> take elements from the stack until I found a matching tag
405:
406: //I need to save the pop-ed elements for the case that there isn't
407: //any matching start tag found
408: ArrayList savedElements = new ArrayList();
409: //this semaphore is used behind the loop to detect whether a
410: //matching start has been found
411: boolean foundStartTag = false;
412:
413: while (!elementsStack.isEmpty()) {
414: SyntaxElement s = (SyntaxElement) elementsStack
415: .pop();
416: savedElements.add(s);
417:
418: Tag start = (Tag) s;
419: Tag end = (Tag) sel;
420:
421: if (s instanceof StartTag
422: && start.getTagName().equals(
423: end.getTagName())) {
424: //found a matching start tag
425: //XXX I am not sure whether this algorith is correct
426: Map attribs = createAttributesMap((StartTag) s);
427: addedElements
428: .add(dtm
429: .addDocumentElement(
430: start
431: .getTagName(),
432: XML_TAG,
433: attribs,
434: start
435: .getElementOffset(),
436: getSyntaxElementEndOffset(end)));
437:
438: foundStartTag = true;
439: break; //break the while loop
440: }
441: }
442:
443: if (!foundStartTag) {
444: //we didn't find any matching start tag =>
445: //return all elements back to the stack
446: for (int i = savedElements.size() - 1; i >= 0; i--) {
447: elementsStack.push(savedElements
448: .get(i));
449: }
450: }
451: }
452: }
453: } else if (sel instanceof EmptyTag) {
454: Map attribs = createAttributesMap((Tag) sel);
455: addedElements.add(dtm.addDocumentElement(
456: ((EmptyTag) sel).getTagName(),
457: XML_EMPTY_TAG, attribs, sel
458: .getElementOffset(),
459: getSyntaxElementEndOffset(sel)));
460: } else if (sel instanceof CDATASectionImpl) {
461: //CDATA section
462: addedElements.add(dtm.addDocumentElement("cdata",
463: XML_CDATA, Collections.EMPTY_MAP, sel
464: .getElementOffset(),
465: getSyntaxElementEndOffset(sel)));
466: } else if (sel instanceof ProcessingInstructionImpl) {
467: //PI section
468: String nodeName = ((ProcessingInstructionImpl) sel)
469: .getNodeName();
470: //if the nodename is not parsed, then the element is somehow broken => do not show it.
471: if (nodeName != null) {
472: addedElements.add(dtm.addDocumentElement(
473: nodeName, XML_PI,
474: Collections.EMPTY_MAP, sel
475: .getElementOffset(),
476: getSyntaxElementEndOffset(sel)));
477: }
478: } else if (sel instanceof DocumentTypeImpl) {
479: //document type <!DOCTYPE xxx [...]>
480: String nodeName = ((DocumentTypeImpl) sel)
481: .getName();
482: //if the nodename is not parsed, then the element is somehow broken => do not show it.
483: if (nodeName != null) {
484: addedElements.add(dtm.addDocumentElement(
485: nodeName, XML_DOCTYPE,
486: Collections.EMPTY_MAP, sel
487: .getElementOffset(),
488: getSyntaxElementEndOffset(sel)));
489: }
490: } else if (sel instanceof CommentImpl) {
491: //comment element <!-- xxx -->
492: //DO NOT CREATE ELEMENT FOR COMMENTS
493: addedElements.add(dtm.addDocumentElement("comment",
494: XML_COMMENT, Collections.EMPTY_MAP, sel
495: .getElementOffset(),
496: getSyntaxElementEndOffset(sel)));
497: } else {
498: //everything else is content
499: addedElements.add(dtm.addDocumentElement("...",
500: XML_CONTENT, Collections.EMPTY_MAP, sel
501: .getElementOffset(),
502: getSyntaxElementEndOffset(sel)));
503: }
504: //find next syntax element
505: // sel = sel.getNext(); //this cannot be used since it chains the results and they are hard to GC then.
506: try {
507: //prevent cycles
508: SyntaxElement prev = null;
509: int add = 0;
510: do {
511: add++;
512: prev = sup.getElementChain(sel
513: .getElementOffset()
514: + sel.getElementLength() + add);
515: } while (prev != null
516: && sel.getElementOffset() >= prev
517: .getElementOffset());
518: sel = prev;
519: } catch (BadLocationException ble) {
520: sel = null;
521: }
522: }
523:
524: //*** elements removal ***
525: // we need to remove those elements which existed before and not exist now
526: //we need to get all descendants from non-skipped elements
527: List existingElements = getDescendantsOfNotSkippedElements(
528: de, skipped);
529: existingElements.add(de);
530:
531: Iterator existingItr = existingElements.iterator();
532: //iterate all existing elements and check if they are still valid
533: while (existingItr.hasNext()) {
534: DocumentElement d = (DocumentElement) existingItr
535: .next();
536: if (!addedElements.contains(d)) {
537: //remove the element - it doesn't longer exist
538: dtm.removeDocumentElement(d, false);
539: if (debug)
540: System.out
541: .println("[xml model] removed element "
542: + d);
543: }
544: }
545:
546: } catch (BadLocationException e) {
547: throw new DocumentModelException(
548: "Error occurred during generation of Document elements",
549: e);
550: }
551: }
552:
553: private List/*<DocumentElement>*/getDescendantsOfNotSkippedElements(
554: DocumentElement de,
555: List/*<DocumentElement>*/skippedElements) {
556: ArrayList desc = new ArrayList();
557: Iterator children = de.getChildren().iterator();
558: while (children.hasNext()) {
559: DocumentElement child = (DocumentElement) children.next();
560: if (!skippedElements.contains(child)) {
561: desc.add(child);
562: desc.addAll(getDescendantsOfNotSkippedElements(child,
563: skippedElements));
564: }
565: }
566: return desc;
567: }
568:
569: private int getSyntaxElementEndOffset(SyntaxElement sel) {
570: //zmenil jsem velikost vsech elementu tak, ze jejich
571: //delka je kratsi o jeden => to resi problem kdyz se zacne psat na end position ->
572: //text se v tomto pripade pridava jeste do elementu pred end positionou
573: //napr:
574: // <a>xxx</a>X
575: //predtim to X padalo do tagu <a>, coz je blbe. Ted to padne za nej.
576: //TODO musi se ale nejak vyresit problem zkracene delky - u text elementu se pri cteni
577: //hodnoty musi pouzit endoffset + 1
578: return sel.getElementOffset() + sel.getElementLength() - 1;
579: }
580:
581: private Map createAttributesMap(Tag tag) {
582: if (tag.getAttributes().getLength() == 0) {
583: return Collections.EMPTY_MAP;
584: } else {
585: HashMap map = new HashMap(tag.getAttributes().getLength());
586: for (int i = 0; i < tag.getAttributes().getLength(); i++) {
587: AttrImpl attr = (AttrImpl) tag.getAttributes().item(i);
588: map.put(attr.getName(), attr.getValue());
589: }
590: return map;
591: }
592: }
593:
594: public static final String XML_TAG = "tag";
595: public static final String XML_EMPTY_TAG = "empty_tag";
596: public static final String XML_CONTENT = "content";
597: public static final String XML_PI = "pi";
598: public static final String XML_CDATA = "cdata";
599: public static final String XML_DOCTYPE = "doctype";
600: public static final String XML_COMMENT = "comment";
601:
602: public static final String XML_ERROR = "error";
603:
604: private static final boolean debug = Boolean
605: .getBoolean("org.netbeans.modules.xml.text.structure.debug");
606: private static final boolean measure = Boolean
607: .getBoolean("org.netbeans.modules.xml.text.structure.measure");
608:
609: }
|