/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.modules.web.core.syntax;

import java.io.File;
import java.util.*;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.EditorKit;
import javax.swing.text.JTextComponent;
import javax.servlet.jsp.tagext.*;
import org.netbeans.api.lexer.Language;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.web.core.syntax.completion.api.JspCompletionItem;
import org.netbeans.modules.web.core.syntax.deprecated.ELTokenContext;
import org.netbeans.modules.web.core.syntax.deprecated.JspDirectiveTokenContext;
import org.netbeans.modules.web.core.syntax.deprecated.JspMultiTokenContext;
import org.netbeans.modules.web.core.syntax.deprecated.JspTagTokenContext;
import org.netbeans.modules.web.core.api.JspContextInfo;
import org.netbeans.modules.web.jsps.parserapi.JspParserAPI.JspOpenInfo;
import org.netbeans.spi.editor.completion.CompletionItem;
import org.openide.filesystems.FileAttributeEvent;
import org.openide.filesystems.FileChangeListener;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileRenameEvent;
import org.openide.loaders.DataObject;
import org.netbeans.modules.web.jsps.parserapi.JspParserAPI;
import org.netbeans.modules.web.jsps.parserapi.PageInfo;
import org.netbeans.editor.*;
import org.netbeans.editor.ext.ExtSyntaxSupport;
import org.netbeans.editor.ext.java.JavaTokenContext;
import org.netbeans.modules.editor.NbEditorUtilities;
import org.netbeans.modules.html.editor.api.completion.HtmlCompletionItem;
import org.openide.filesystems.FileUtil;
import org.openide.modules.InstalledFileLocator;
import org.openide.text.CloneableEditorSupport;
import org.openide.util.WeakListeners;

/**
 *
 * @author  Petr Jiricka, Petr Nejedly
 * @author Marek.Fukala@Sun.COM
 */
public class JspSyntaxSupport extends ExtSyntaxSupport implements FileChangeListener {

    /** ErrorManager shared by whole module (package) for logging */
    static final Logger err =
            Logger.getLogger("org.netbeans.modules.web.jspsyntax"); // NOI18N

    /* Constants for various contexts in the text from the point of
    view of JSP completion.*/

    /** Completion context for JSP tags (standard or custom) */
    public static final int TAG_COMPLETION_CONTEXT = 1;
    /** Completion context for JSP end tags (standard or custom) */
    public static final int ENDTAG_COMPLETION_CONTEXT = 2;
    /** Completion context for JSP directives */
    public static final int DIRECTIVE_COMPLETION_CONTEXT = 3;
    /** Completion context for JSP comments */
    public static final int COMMENT_COMPLETION_CONTEXT = 4;
    /** Completion context for other JSP text - such as body of custom tags
     * with TAG_DEPENDENT body content. */
    public static final int TEXT_COMPLETION_CONTEXT = 5;
    /** Completion context for the content language */
    public static final int CONTENTL_COMPLETION_CONTEXT = 6;
    /** Completion context for the scripting language */
    public static final int SCRIPTINGL_COMPLETION_CONTEXT = 7;
    /** Completion context for error */
    public static final int ERROR_COMPLETION_CONTEXT = 8;
    /** Completion context for expression language */
    public static final int EL_COMPLETION_CONTEXT = 9;



    private static final String STANDARD_JSP_PREFIX = "jsp";    // NOI18N
    /** Data for completion: TreeMap for standard JSP tags
     * (tag name, array of attributes). */
    private static TagInfo[] standardJspTagDatas;

    private static TagInfo[] standardTagTagDatas;
    /** Data for completion, when the jsp page is in XML syntax
     **/
    private static TagInfo[] xmlJspTagDatas;

    /** Data for completion, when the tag file is in XML syntax
     **/
    private static TagInfo[] xmlTagFileTagDatas;

    /** Data for completion: TreeMap for JSP directives
     * (directive name, array of attributes). */
    private static TagInfo[] directiveJspData;
    private static TagInfo[] directiveTagFileData;

    /** Mapping the URI of tag library -> URL where the help files are.
     */
    private static HashMap helpMap = null;

    private static final TokenID[] JSP_BRACKET_SKIP_TOKENS = new TokenID[] {
        JavaTokenContext.LINE_COMMENT,
        JavaTokenContext.BLOCK_COMMENT,
        JavaTokenContext.CHAR_LITERAL,
        JavaTokenContext.STRING_LITERAL,
        JspTagTokenContext.ATTR_VALUE,
        JspTagTokenContext.COMMENT
    };

    protected FileObject fobj;

    /** Special bracket finder is used when caret is in JSP context */
    private boolean useCustomBracketFinder = true;

    /** Creates new JspSyntaxSupport */
    public static synchronized JspSyntaxSupport get(Document doc) {
        JspSyntaxSupport sup = (JspSyntaxSupport)doc.getProperty(JspSyntaxSupport.class);
        if(sup == null) {
            sup = new JspSyntaxSupport((BaseDocument)doc);
            doc.putProperty(JspSyntaxSupport.class, sup);
            }
        return sup;
    }

    public JspSyntaxSupport(BaseDocument doc) {
        super(doc);
        fobj = null;
        if (doc != null){
            initFileObject();
        }

    }

    private void initFileObject() {
        DataObject dobj = NbEditorUtilities.getDataObject(getDocument());
            if(dobj != null)  {
                fobj = NbEditorUtilities.getDataObject(getDocument()).getPrimaryFile();
                fobj.addFileChangeListener(WeakListeners.create(FileChangeListener.class, this, fobj));
            }
    }

    public String[] getImports(){
        JspParserAPI.ParseResult pre = getParseResult();
        if (pre != null){
            PageInfo pi = pre.getPageInfo();
            if(pi == null) {
                //report error but do not break the entire CC
                err.log(Level.WARNING, null, new NullPointerException("PageInfo obtained from JspParserAPI.ParseResult is null"));
                return null;
            }
            List<String> imports = pi.getImports();
            return imports.toArray(new String[imports.size()]);
        }

        return null;
    }

    public boolean isXmlSyntax(){
        JspContextInfo jspCO = JspContextInfo.getContextInfo(fobj);
        if(jspCO == null) {
            return false;
        }
        JspOpenInfo jspOpenInfo = jspCO.getCachedOpenInfo(fobj, false);
        if(jspOpenInfo == null) {
            return false;
        }
        return jspOpenInfo.isXmlSyntax();
    }

    protected JspParserAPI.ParseResult getParseResult() {
        JspParserAPI.ParseResult result = JspUtils.getCachedParseResult(fobj, true, false);
        if (result == null) {
            result = JspUtils.getCachedParseResult(fobj, false, false);
        }
        return result;
    }

    /** Returns a map of prefix -> URI that maps tag libraries on prefixes.
     * For the XML syntax this mapping may only be approximate.
     */
    protected Map getPrefixMapper() {
        // PENDING - must also take xmlPrefixMapper into account
        JspParserAPI.ParseResult result = getParseResult();
        Map prefixMapper = null;
        if (result != null && result.getPageInfo() != null) {
            //if (result.isParsingSuccess()) {
            // PENDING - can we somehow get incomplete parsed information ?
            if (result.getPageInfo().getXMLPrefixMapper().size() > 0) {
                prefixMapper = result.getPageInfo().getApproxXmlPrefixMapper();
                if (prefixMapper.size() == 0){
                    prefixMapper = result.getPageInfo().getXMLPrefixMapper();
                }
                prefixMapper.putAll(result.getPageInfo().getJspPrefixMapper());

            } else {
                prefixMapper = result.getPageInfo().getJspPrefixMapper();
            }
            //}
        }
        return prefixMapper;
    }

    private Map getTagLibraries(boolean requiresFresh) {
        //refresh tag libraries mappings - this call causes the WebAppParseSupport to refresh taglibs mapping
        getTagLibraryMappings();
        //if requiresFresh force the parser to update the parse information for the file
        JspParserAPI.ParseResult result = JspUtils.getCachedParseResult(fobj, false, requiresFresh, requiresFresh);
        if (result != null) {
            PageInfo pi = result.getPageInfo();
            if(pi == null) {
                //report error but do not break the entire CC
                err.log(Level.INFO, null, new NullPointerException("PageInfo obtained from JspParserAPI.ParseResult is null"));
                return null;
            } else {
                return pi.getTagLibraries();
            }
        }

        return null; //an error
    }

    private TagInfo[] getSortedTagInfos(TagInfo[] tinfos) {
        Arrays.sort(tinfos, new Comparator() {
            public int compare(Object o1, Object o2) {
                TagInfo ti1 = (TagInfo)o1;
                TagInfo ti2 = (TagInfo)o2;
                String tname1 = (ti1.getDisplayName() == null ? ti1.getTagName() : ti1.getDisplayName());
                String tname2 = (ti2.getDisplayName() == null ? ti2.getTagName() : ti2.getDisplayName());
                if(tname1 == null || tname2 == null) return 0;
                return tname1.compareTo(tname2);
            }
            @Override
            public boolean equals(Object o) {
                return o.equals(this);
            }
        });
        return tinfos;
    }

    private TagLibraryInfo getTagLibrary(String prefix, boolean requiresFresh) {
        Map mapper = getPrefixMapper();
        if (mapper != null) {
            Object uri = mapper.get(prefix);
            if (uri != null) {
                Map taglibs = getTagLibraries(requiresFresh);
                if (taglibs != null) {
                    return (TagLibraryInfo)taglibs.get(uri);
                }
            }
        }
        return null;
    }

    @Override
    protected SyntaxSupport createSyntaxSupport(Class syntaxSupportClass) {
        SyntaxSupport support = super.createSyntaxSupport(syntaxSupportClass);
        if (support != null)
            return support;

        EditorKit kit;
        // try the content language support
        kit = CloneableEditorSupport.getEditorKit("text/html"); //NOI18N
        if (kit instanceof BaseKit) {
            support = ((BaseKit)kit).createSyntaxSupport(getDocument());
            if (support != null)
                return support;
        }
        // try the scripting language support
        kit = CloneableEditorSupport.getEditorKit("text/x-java"); //NOI18N
        if (kit instanceof BaseKit) {
            support = ((BaseKit)kit).createSyntaxSupport(getDocument());
            if (support != null)
                return support;
        }
        return null;
    }

    /** This method decides what kind of completion (html, java, jsp-tag, ...) should be opened
     * or whether the completion window should be closed if it is opened.
     */
    @Override
    public int checkCompletion(JTextComponent target, String typedText, boolean visible ) {

        char first = typedText.charAt(0); //get typed char

        TokenItem item = null; //get token on the cursor
        try{
            item = getItemAtOrBefore(target.getCaret().getDot());
        }catch(BadLocationException e) {
            return COMPLETION_HIDE;
        }
        if (item == null) return COMPLETION_HIDE;

        TokenContextPath tcp = item.getTokenContextPath();

        if(tcp.contains(JavaTokenContext.contextPath)) {
            return COMPLETION_CANCEL; //the JavaCompletionProvider handles this
        }

        //JSP tag or directive
        if(tcp.contains(JspTagTokenContext.contextPath)) {
            //need to distinguish tag/end_tag/directive - search back throught the token chain for <%@ , </ or < tokens
            TokenItem tracking = item;

            //the maxBacktrace number says how far back we are willing to look for
            //the start tag tokens <%, </ and < (simply the JSP tag or directive beginning).
            //There may happen a situation when user starts to write a tag that the a large
            //part of the document behind the cursor is recognized as a jsp tag.
            //In such a case typing somewhere in the end of this 'badly' recognized block
            //would slow down the typing rapidy due to the backtracking oven a long token chain.
            //
            //So we suppose that tag has less than 20 attributes (and one attribute-value pair consumes 4 tokens)
            int maxBacktrace = 20 * 4;

            do {
                //test whether the token is not an error token
                if(tracking.getTokenID() == JspTagTokenContext.ERROR) return COMPLETION_HIDE;

                String image = tracking.getImage();
                //System.out.println("tracking: " + tracking);

                //this is a case when use types %> which is recognized as a JspTagToken, but before that there are java tokens
                if(image.equals("%>")) return COMPLETION_HIDE;

                if(tracking.getImage().startsWith("<%")) {
                    //we are in a directive
                    if (err.isLoggable(Level.FINE)) err.log(Level.FINE, "DIRECTIVE_COMPLETION_CONTEXT");   // NOI18N

                    //open completion also in such a case: <%=|
                    if( !visible && first == '=' && tracking.getImage().equals("<%")) return COMPLETION_POPUP;

                    if( !visible && first == '%' || first == '@' || first == ' ' ) return COMPLETION_POPUP;
                    if( visible && first == '=' || first == '>' ) return COMPLETION_HIDE;
                    return visible ? COMPLETION_POST_REFRESH : COMPLETION_CANCEL;
                }
                if(tracking.getImage().equals("<")) {
                    //we are in a tag
                    if (err.isLoggable(Level.FINE)) err.log(Level.FINE, "TAG_COMPLETION_CONTEXT");   // NOI18N
                    if( !visible && first == ' ' || first == ':' ) return COMPLETION_POPUP;
                    if( visible && first == '>' ) return COMPLETION_HIDE;
                    return visible ? COMPLETION_POST_REFRESH : COMPLETION_CANCEL;
                }
                if(tracking.getImage().equals("</")) {
                    //we are in an end tag
                    if (err.isLoggable(Level.FINE)) err.log(Level.FINE, "ENDTAG_COMPLETION_CONTEXT");   // NOI18N
                    if( visible && first == '>' ) return COMPLETION_HIDE;
                    return visible ? COMPLETION_POST_REFRESH : COMPLETION_CANCEL;
                }
                //test whether we are still in the tag context
                if(!tracking.getTokenContextPath().contains(JspTagTokenContext.contextPath)) {
                    if (err.isLoggable(Level.FINE)) err.log(Level.FINE, "We are out of jsp tag without finding any tag start token!");
                    break;
                }

                if(tracking.getImage().equals(">")) {
                    try {
                        //check if the cursor is behind an open tag
                        SyntaxElement se = getElementChain(tracking.getOffset());
                        if(se instanceof SyntaxElement.Tag) {
                            return COMPLETION_POPUP;
                        }
                    }catch(BadLocationException e) {
                        //do nothing
                    }
                    return COMPLETION_HIDE;
                }

                //search previous token
                tracking = tracking.getPrevious();

            } while((maxBacktrace-- > 0) && (tracking != null));

        }//eof JSP tag

        if(tcp.contains(ELTokenContext.contextPath)) {
            if(first == '.') {
                return COMPLETION_POPUP;
            }
        }

        return COMPLETION_HIDE;
    }

    /** Returns offset where the next offset after this offset starts. */
    private final int getTokenEnd( TokenItem item ) {
        if (item == null)
            return 0; //getDocument().getLength();
        return item.getOffset() + item.getImage().length();
    }

    /** Filters list of strings so only strings starting
     * with a given prefix are returned in the new List. */
    public static final List<Object> filterList(List<? extends Object> toFilter, String prefix) {
        List<Object> newList = new ArrayList<Object>();
        Object item;
        for (int i = 0; i < toFilter.size(); i++) {
            item = toFilter.get(i);
            String txt;
            if (item instanceof TagInfo)
                txt = ((TagInfo)item).getTagName();
            else if (item instanceof TagAttributeInfo)
                txt = ((TagAttributeInfo)item).getName();
            else
                txt = (String)item;

            if (txt != null && txt.startsWith(prefix)) {
                newList.add(item);
            }
        }
        return newList;
    }

     /** Filters list of strings so only strings starting
     * with a given prefix are returned in the new List. */
    public static final List<String> filterStrings(List<String> toFilter, String prefix) {
        List<String> newList = new ArrayList<String>();
        for(String val : toFilter) {
            if(val.startsWith(prefix)) {
                newList.add(val);
            }
        }
        return newList;
    }

    /** Gets all 'jsp prefixes' whose 'string prefix' matches complPrefix as a list of Strings. */
    public final List<Object> getTagPrefixes(String complPrefix) {
        return filterList(getAllTagPrefixes(), complPrefix);
    }

    /** Gets all tags whose 'string prefix' matches complPrefix as a list of Strings.
     * Assumes that complPrefix also includes the 'jsp prefix'.
     */
    public final List getTags(String complPrefix) {
        int colonIndex = complPrefix.indexOf(":");  // NOI18N
        if (colonIndex == -1)
            throw new IllegalArgumentException();
        return getTags(complPrefix.substring(0, colonIndex),
                complPrefix.substring(colonIndex + 1));
    }

    /** Gets all tags whose 'string prefix' matches complPrefix and whose 'jsp prefix'
     * is tagPrefix as a list of Strings.
     * Assumes that complPrefix does not include the 'jsp prefix'.
     */
    public final List getTags(String tagPrefix, String complPrefix) {
        return filterList(getAllTags(tagPrefix, true), complPrefix);
    }

    /** Gets attributes for tag whose prefix + name
     * is tagPrefixName as a list of Strings.
     * The attribute's 'string prefix' must match complPrefix.
     */
    public final List getTagAttributes(String tagPrefixName, String complPrefix) {
        int colonIndex = tagPrefixName.indexOf(":");    // NOI18N
        if (colonIndex == -1)
            throw new IllegalArgumentException();
        return getTagAttributes(tagPrefixName.substring(0, colonIndex),
                tagPrefixName.substring(colonIndex + 1), complPrefix);
    }

    /** Gets attributes for tag whose 'jsp prefix'
     * is tagPrefix and whose tag name is tagName as a list of Strings.
     * The attribute's 'string prefix' must match complPrefix.
     */
    protected final List getTagAttributes(String tagPrefix, String tagName, String complPrefix) {
        return filterList(getAllTagAttributes(tagPrefix, tagName), complPrefix);
    }

    /** Gets all directives whose 'string prefix' matches complPrefix as a list of Strings. */
    public final List getDirectives(String complPrefix) {
        return filterList(getAllDirectives(), complPrefix);
    }

    /** Gets attributes for directive <code>directive</code> as a list of Strings.
     * The attribute's 'string prefix' must match complPrefix.  */
    public final List getDirectiveAttributes(String directive, String complPrefix) {
        return filterList(getAllDirectiveAttributes(directive), complPrefix);
    }

    /**
     *  Returns a list of strings - prefixes available in this support context (JSP file).
     */
    protected List getAllTagPrefixes() {
        List items = new ArrayList();

        // jsp: prefix
        items.add(STANDARD_JSP_PREFIX);

        Map mapper = getPrefixMapper();
        if (mapper != null) {
            // sort it
            TreeSet ts = new TreeSet();
            ts.addAll(mapper.keySet());
            ts.remove("jsp"); // remove jsp prefix when is declared in xml syntax
            items.addAll(ts);
        }
        // prefixes for tag libraries
/*        TagLibParseSupport support = (dobj == null) ?
            null : (TagLibParseSupport)dobj.getCookie(TagLibParseSupport.class);
        if (support != null) {
            // add all prefixes from the support
            TagLibParseSupport.TagLibData[] tagLibData = support.getTagLibEditorData().getTagLibData();
            for (int i = 0; i < tagLibData.length; i++)
                items.add(tagLibData[i].getPrefix());
        }
 */
        return items;
    }

    /** This method merges tagInfos of tags and file tags from a tagLibrary.
     * @param tagLibrary
     * @return array of TagInfo of all tags and tag files from the library
     * or an empty array of if there is not any tag or tagfile defined in the library.
     */
    private TagInfo[] getAllTagInfos(TagLibraryInfo tagLibrary) {

        // if there is not any tag or file tag, then the empty array is returned
        TagInfo[] allTags = new TagInfo[0];

        if (tagLibrary != null) {
            int tagInfosLength = 0;
            int tagAllInfosLength = 0;
            TagInfo[] tagInfos = tagLibrary.getTags();
            TagFileInfo[] tagFileInfos = tagLibrary.getTagFiles();

            if (tagInfos != null) { // it can be null, when the jsp parser finished unexpectedly
                tagInfosLength = tagInfos.length;
                tagAllInfosLength = tagInfosLength;
            }
            if (tagFileInfos != null) { // it can be null, when the jsp parser finished unexpectedly
                tagAllInfosLength = tagAllInfosLength + tagFileInfos.length;
            }

            allTags = new TagInfo[tagAllInfosLength];

            if (tagInfos != null) {
                for (int i = 0; i < tagInfosLength; i++) {
                    allTags[i] = tagInfos[i];
                }
            }
            if (tagFileInfos != null) {
                for (int i = 0; i < tagFileInfos.length; i++) {
                    allTags[tagInfosLength + i] = tagFileInfos[i].getTagInfo();
                }
            }
        }
        return allTags;
    }

    /**  Returns a list of strings - tag names available for a particular prefix.
     */
    protected List getAllTags(String prefix, boolean requiresFresh) {
        List items = new ArrayList();

        // standard JSP tags (jsp:)
        initCompletionData();
        if (STANDARD_JSP_PREFIX.equals(prefix)) {
            TagInfo[] stanTagDatas = getTagInfos();
            for (int i=0; i<stanTagDatas.length; i++) {
                items.add(stanTagDatas[i]);
            }
        }

        TagLibraryInfo info = getTagLibrary(prefix, requiresFresh);
        TagInfo[] tags = null;
        if (info != null) {
            tags = getAllTagInfos(info);
        }
        if (tags != null) {
            tags = getSortedTagInfos(tags);
            String url = (String)helpMap.get(info.getURI());
            if (url != null && !url.equals("")){
                for (int i = 0; i < tags.length; i++) {
                    items.add(new TagInfo(tags[i].getTagName(),
                            tags[i].getTagClassName(), tags[i].getBodyContent(),
                            url + tags[i].getTagName() + ".html#tag-start-" + tags[i].getTagName()
                            + "#tag-end-" + tags[i].getTagName(), info,
                            tags[i].getTagExtraInfo(), tags[i].getAttributes(),
                            tags[i].getDisplayName(), tags[i].getSmallIcon(), tags[i].getLargeIcon(),
                            tags[i].getTagVariableInfos(), tags[i].hasDynamicAttributes()));
                }
            } else {
                for (int i = 0; i < tags.length; i++) {
                    items.add(tags[i]);
                }
            }
        }
        return items;
    }

    /** Should be overriden ny subclasses to support JSP 1.1.
     *  Returns a list of strings - attribute names available for a particular prefix and tag name.
     */
    protected List getAllTagAttributes(String prefix, String tag) {
        List items = new ArrayList();

        // attributes for standard JSP tags (jsp:)
        initCompletionData();
        if (STANDARD_JSP_PREFIX.equals(prefix)) {
            TagInfo[] stanTagDatas = getTagInfos();
            for (int i=0; i<stanTagDatas.length; i++) {
                if (stanTagDatas[i].getTagName().equals(tag)) {
                    TagAttributeInfo[] attrs = stanTagDatas[i].getAttributes();
                    for (int j=0; j<attrs.length; j++)
                        items.add(attrs[j]);
                    break;
                }
            }
        }

        TagLibraryInfo info = getTagLibrary(prefix, true);
        if (info != null) {
            TagInfo tagInfo = info.getTag(tag);
            if (tagInfo == null) {
                TagFileInfo tagFile = info.getTagFile(tag);
                if (tagFile != null) {
                    tagInfo = tagFile.getTagInfo();
                }
            }
            if (tagInfo != null) {
                TagAttributeInfo[] attributes = tagInfo.getAttributes();
                String url = (String)helpMap.get(tagInfo.getTagLibrary().getURI());
                if (url != null && !url.equals("")){
                    for (int i = 0; i < attributes.length; i++) {
                        items.add(new TagAttributeInfo(attributes[i].getName(),
                                attributes[i].isRequired(),
                                url + tagInfo.getTagName() + ".html#attribute-start-" + attributes[i].getName()
                                + "#attribute-end-" + attributes[i].getName(),
                                attributes[i].canBeRequestTime(),
                                attributes[i].isFragment()));
                    }
                } else {
                    for (int i = 0; i < attributes.length; i++)
                        items.add(attributes[i]);
                }
            }
        }
        return items;
    }



    /** Should be overriden ny subclasses to support JSP 1.1. */
    protected List<TagInfo> getAllDirectives() {
        initCompletionData();
        List<TagInfo> items = new ArrayList<TagInfo>();

        //Is xml syntax? => return nothing.
        if (isXmlSyntax()) return items;

        TagInfo[] directiveData;
        if(NbEditorUtilities.getMimeType(getDocument()).equals(JspUtils.TAG_MIME_TYPE))
            directiveData = directiveTagFileData;
        else {
            directiveData = directiveJspData;

        }
        for (int i = 0; i < directiveData.length; i++){
            items.add(directiveData[i]);
        }
        return items;
    }

    /** Should be overriden ny subclasses to support JSP 1.1. */
    protected List getAllDirectiveAttributes(String directive) {
        initCompletionData();
        List items = new ArrayList();
        //Is xml syntax? => return nothing.
        if (isXmlSyntax()) return items;

        TagInfo[] directiveData;
        if(NbEditorUtilities.getMimeType(getDocument()).equals(JspUtils.TAG_MIME_TYPE))
            directiveData = directiveTagFileData;
        else
            directiveData = directiveJspData;
        for (int i=0; i<directiveData.length; i++) {
            if (directiveData[i].getTagName().equals(directive)) {
                TagAttributeInfo[] attrs = directiveData[i].getAttributes();
                for (int j=0; j<attrs.length; j++)
                    items.add(attrs[j]);
                break;
            }
        }
        return items;
    }

    public PageInfo.BeanData[] getBeanData() {
        JspParserAPI.ParseResult result = getParseResult();
        if (result != null) {
            PageInfo pageInfo = result.getPageInfo();
            //pageInfo can be null in some cases when the parser cannot parse
            //the webmodule or the page itself
            if(pageInfo != null) {
                return pageInfo.getBeans();
            }
        }
        /*TagLibParseSupport support = (dobj == null) ?
            null : (TagLibParseSupport)dobj.getCookie(TagLibParseSupport.class);
        return support.getTagLibEditorData().getBeanData();*/
        return null;
    }

    public boolean isErrorPage() {
        JspParserAPI.ParseResult result = getParseResult();
        if (result != null) {
            if (result.getPageInfo() != null)
                return result.getPageInfo().isErrorPage();
        }
        /*TagLibParseSupport support = (dobj == null) ?
            null : (TagLibParseSupport)dobj.getCookie(TagLibParseSupport.class);
        return support.getTagLibEditorData().isErrorPage ();*/
        return false;
    }


    /**
     * The mapping of the 'global' tag library URI to the location
     * (resource path) of the TLD associated with that tag library.
     * The location is returned as a String array:
     *    [0] The location
     *    [1] If the location is a jar file, this is the location
     *        of the tld.
     */
    public Map getTagLibraryMappings() {
        if (fobj == null) {
            return null;
        }
        return JspUtils.getTaglibMap(fobj);
    }

    private static void initHelp(){
        if (helpMap == null){
            String url="";
            File f = InstalledFileLocator.getDefault().locate("docs/jstl11-doc.zip", "org.netbeans.modules.web.core.syntax", false); //NoI18N
            if (f != null){
                try {
                    URL urll = f.toURL();
                    urll = FileUtil.getArchiveRoot(urll);
                    url = urll.toString();
                } catch (java.net.MalformedURLException e){
                    err.log(Level.WARNING, null, e);
                    // nothing to do
                }
            }
            helpMap = new HashMap();
            //           The URI from the tld file          URL, where are the files for the library
            helpMap.put("http://java.sun.com/jsp/jstl/core", url + "c/");
            helpMap.put("http://java.sun.com/jstl/core", url + "c/");
            helpMap.put("http://java.sun.com/jstl/core_rt", url + "c_rt/");
            helpMap.put("http://java.sun.com/jsp/jstl/fmt", url + "fmt/");
            helpMap.put("http://java.sun.com/jstl/fmt", url + "fmt/");
            helpMap.put("http://java.sun.com/jstl/fmt_rt", url + "fmt_rt/");
            helpMap.put("http://java.sun.com/jsp/jstl/functions", url + "fn/");
            helpMap.put("http://java.sun.com/jstl/functions", url + "fn/");
            helpMap.put("http://jakarta.apache.org/taglibs/standard/permittedTaglibs", url+"permittedTaglibs/");
            helpMap.put("http://jakarta.apache.org/taglibs/standard/scriptfree", url+ "scriptfree/");
            helpMap.put("http://java.sun.com/jsp/jstl/sql", url + "sql/");
            helpMap.put("http://java.sun.com/jstl/sql", url + "sql/");
            helpMap.put("http://java.sun.com/jstl/sql_rt", url + "sql_rt/");
            helpMap.put("http://java.sun.com/jsp/jstl/xml", url + "x/");
            helpMap.put("http://java.sun.com/jstl/xml", url + "x/");
            helpMap.put("http://java.sun.com/jstl/xml_rt", url + "x_rt/");
            f = InstalledFileLocator.getDefault().locate("docs/jsf12-tlddoc.zip", "org.netbeans.modules.web.core.syntax", false); //NoI18N
            if (f != null){
                try {
                    URL urll = f.toURL();
                    urll = FileUtil.getArchiveRoot(urll);
                    url = urll.toString();
                    helpMap.put("http://java.sun.com/jsf/html", url + "h/");
                    helpMap.put("http://java.sun.com/jsf/core", url + "f/");
                } catch (java.net.MalformedURLException e){
                    err.log(Level.WARNING, null, e);
                    // nothing to do
                }
            }
            f = InstalledFileLocator.getDefault().locate("docs/struts-tags.zip", "org.netbeans.modules.web.core.syntax", false);
            if (f != null){
                try {
                    URL urll = f.toURL();
                    urll = FileUtil.getArchiveRoot(urll);
                    url = urll.toString();
                    helpMap.put("http://jakarta.apache.org/struts/tags-bean", url + "bean/");
                    helpMap.put("http://struts.apache.org/tags-bean", url + "bean/");
                    helpMap.put("/WEB-INF/struts-bean.tld", url + "bean/");
                    helpMap.put("http://jakarta.apache.org/struts/tags-html", url + "html/");
                    helpMap.put("http://struts.apache.org/tags-html", url + "html/");
                    helpMap.put("/WEB-INF/struts-html.tld", url + "html/");
                    helpMap.put("http://jakarta.apache.org/struts/tags-logic", url + "logic/");
                    helpMap.put("http://struts.apache.org/tags-logic", url + "logic/");
                    helpMap.put("/WEB-INF/struts-logic.tld", url + "logic/");
                    helpMap.put("http://jakarta.apache.org/struts/tags-nested", url + "nested/");
                    helpMap.put("http://struts.apache.org/tags-nested", url + "nested/");
                    helpMap.put("/WEB-INF/struts-nested.tld", url + "nested/");
                    helpMap.put("http://jakarta.apache.org/struts/tags-tiles", url + "tiles/");
                    helpMap.put("http://struts.apache.org/tags-tiles", url + "tiles/");
                    helpMap.put("/WEB-INF/struts-tiles.tld", url + "tiles/");
                } catch (java.net.MalformedURLException e){
                    err.log(Level.WARNING, null, e);
                    // nothing to do
                }
            }

        }

    }

    private static void initCompletionData() {
        if (helpMap == null)
            initHelp();
        String url = "";           // NOI18N

         if (directiveJspData == null){
            directiveJspData = new TagInfo[] {
                new TagInfo("include", null, TagInfo.BODY_CONTENT_EMPTY, url+"syntaxref209.html#1003408#8975", null, null,            // NOI18N
                        new TagAttributeInfo[] { new TagAttributeInfo("file", true, url + "syntaxref209.html#16836#10636", false)}),           // NOI18N
                        new TagInfo("page", null, TagInfo.BODY_CONTENT_EMPTY, url+"syntaxref2010.html", null, null,            // NOI18N
                        new TagAttributeInfo[] { new TagAttributeInfo("autoFlush", false, url+"syntaxref2010.html#15673#15675", false),           // NOI18N
                        new TagAttributeInfo("buffer", false, url+"syntaxref2010.html#15671#15673", false),           // NOI18N
                        new TagAttributeInfo("contentType", false, url+"syntaxref2010.html#15683#1001361", false),           // NOI18N
                        new TagAttributeInfo("errorPage", false, url+"syntaxref2010.html#15679#15681", false),              // NOI18N
                        new TagAttributeInfo("extends", false, url+"syntaxref2010.html#15665#16862", false),           // NOI18N
                        new TagAttributeInfo("import", false, url+"syntaxref2010.html#16862#15669", false),           // NOI18N
                        new TagAttributeInfo("info", false, url+"syntaxref2010.html#15677#15679", false),           // NOI18N
                        new TagAttributeInfo("isELIgnored", false, url+"syntaxref2010.html#1011216#18865", false), // NOI18N
                        new TagAttributeInfo("isErrorPage", false, url+"syntaxref2010.html#15681#15683", false), // NOI18N
                        new TagAttributeInfo("isThreadSafe", false, url+"syntaxref2010.html#15675#15677", false), // NOI18N
                        new TagAttributeInfo("language", false, url+"syntaxref2010.html#15663#15665", false), // NOI18N
                        new TagAttributeInfo("pageEncoding", false, url+"syntaxref2010.html#1001361#1011216", false), // NOI18N
                        new TagAttributeInfo("session", false, url+"syntaxref2010.html#15669#15671", false)}), // NOI18N
                        new TagInfo("taglib", null, TagInfo.BODY_CONTENT_EMPTY, url+"syntaxref2012.html#1003416#1002041", null, null,  // NOI18N
                        new TagAttributeInfo[] { new TagAttributeInfo("prefix", true, url+"syntaxref2012.html#1011290#1002041", false), // NOI18N
                        new TagAttributeInfo("uri", false, url+"syntaxref2012.html#10721#1011294", false), // NOI18N
                        new TagAttributeInfo("tagdir", false, url + "syntaxref2012.html#1011294#1011290", false)}) // NOI18N
            };
        }

         if (directiveTagFileData == null){
            directiveTagFileData = new TagInfo[]{
                new TagInfo("attribute", null, TagInfo.BODY_CONTENT_EMPTY, url + "syntaxref208.html", null, null,                        // NOI18N
                        new TagAttributeInfo[] { new TagAttributeInfo("description", false, url + "syntaxref208.html#1004672", false),                      // NOI18N
                        new TagAttributeInfo("fragment", false, url + "syntaxref208.html#1004657#1004666", false),                         // NOI18N
                        new TagAttributeInfo("name", true, url + "syntaxref208.html#1004648#1004655", false),                              // NOI18N
                        new TagAttributeInfo("required", false, url + "syntaxref208.html#1004655#1004657", false),                             // NOI18N
                        new TagAttributeInfo("rtexprvalue", false, url + "syntaxref208.html#1004666#1004669", false),                          // NOI18N
                        new TagAttributeInfo("type", false, url + "syntaxref208.html#1004669#1004672", false)}),                                   // NOI18N
                        directiveJspData[0],
                        new TagInfo("tag", null, TagInfo.BODY_CONTENT_EMPTY, url + "syntaxref2011.html", null, null,                              // NOI18N
                        new TagAttributeInfo[] { new TagAttributeInfo("body-content", false, url + "syntaxref2011.html#1005164#005172", false),                 // NOI18N
                        new TagAttributeInfo("description", false, url + "syntaxref2011.html#1005196#1005198", false),                  // NOI18N
                        new TagAttributeInfo("display-name", false, url + "syntaxref2011.html#1005161#1005164", false),                 // NOI18N
                        new TagAttributeInfo("dynamic-attributes", false, url + "syntaxref2011.html#005172#1005190", false),           // NOI18N
                        new TagAttributeInfo("example", false, url + "syntaxref2011.html#1005198#1005201", false),                      // NOI18N
                        new TagAttributeInfo("import", false, url + "syntaxref2011.html#1005203#1005209", false),                       // NOI18N
                        new TagAttributeInfo("isELIgnored", false, url + "syntaxref2011.html#1005214#1005291#1005291", false),                  // NOI18N
                        //new TagAttributeInfo("isScriptingEnabled", false, url + "syntaxref2011.html#", false),            // NOI18N
                        new TagAttributeInfo("large-icon", false, url + "syntaxref2011.html#1005193#1005196", false),                   // NOI18N
                        new TagAttributeInfo("language", false, url + "syntaxref2011.html#1005201#1005203", false),                     // NOI18N
                        new TagAttributeInfo("pageEncoding", false, url + "syntaxref2011.html#1005209#1005214", false),                  // NOI18N
                        new TagAttributeInfo("small-icon", false, url + "syntaxref2011.html#1005190#1005193", false)}),                     // NOI18N
                        directiveJspData[2],
                        new TagInfo("variable", null, TagInfo.BODY_CONTENT_EMPTY, url + "syntaxref2013.html#15694#1003563", null, null,                             // NOI18N
                        new TagAttributeInfo[] { new TagAttributeInfo("alias", false, url + "syntaxref2013.html#1005914#1005956", false),                          // NOI18N
                        new TagAttributeInfo("declare", false, url + "syntaxref2013.html#1006001#1006019", false),                          // NOI18N
                        new TagAttributeInfo("description", false, url + "syntaxref2013.html#1005991#1003563", false),                      // NOI18N
                        new TagAttributeInfo("name-given", false, url + "syntaxref2013.html#1003561#1005914", false),                       // NOI18N
                        new TagAttributeInfo("scope", false, url + "syntaxref2013.html#1006019#1005991", false),                            // NOI18N
                        new TagAttributeInfo("variable-class", false, url + "syntaxref2013.html#1005956#1006001", false)})                  // NOI18N
            };
        }

        if (standardJspTagDatas == null) {
            final String helpFiles = "docs/syntaxref20.zip"; //NoI18N
            File f = InstalledFileLocator.getDefault().locate(helpFiles, "org.netbeans.modules.web.core.syntax", true); //NoI18N
            if (f != null){
                try {
                    URL urll = f.toURL();
                    urll = FileUtil.getArchiveRoot(urll);
                    url = urll.toString();
                } catch (java.net.MalformedURLException e){
                    err.log(Level.WARNING, null, e);
                    // nothing to do
                }
            }
            standardJspTagDatas = new TagInfo[] {
                new TagInfo("attribute", null, TagInfo.BODY_CONTENT_JSP, url + "syntaxref2014.html",             // NOI18N
                        null, null, new TagAttributeInfo[] { new TagAttributeInfo("name", true, url + "syntaxref2014.html#1003581#1006483", false),           // NOI18N
                        new TagAttributeInfo("trim", false, url + "syntaxref2014.html#1006483#1003583", false)}),           // NOI18N
                new TagInfo("body", null, TagInfo.BODY_CONTENT_JSP, url + "syntaxref2015.html#1006731#1003768",             // NOI18N
                        null, null, new TagAttributeInfo[]{}),
                new TagInfo("element", null, TagInfo.BODY_CONTENT_JSP, url + "syntaxref2016.html#1003696#1003708",             // NOI18N
                        null, null, new TagAttributeInfo[] { new TagAttributeInfo("name", true, url + "syntaxref2016.html#1003706#1003708", false)}),           // NOI18N
                new TagInfo("expression", null, TagInfo.BODY_CONTENT_JSP, url+"syntaxref205.html#1004353#11268",                 // NOI18N
                        null, null, new TagAttributeInfo[] {}),
                new TagInfo("fallback", null, TagInfo.BODY_CONTENT_JSP, url + "syntaxref2023.html#11583#19029",    // NOI18N
                        null, null, new TagAttributeInfo[] {}),
                new TagInfo("forward", null, TagInfo.BODY_CONTENT_JSP, url + "syntaxref2018.html#1003349#15708", // NOI18N
                        null, null, new TagAttributeInfo[] { new TagAttributeInfo("page", true, url + "syntaxref2018.html#15704#15708", true)}),       // NOI18N
                new TagInfo("getProperty", null, TagInfo.BODY_CONTENT_EMPTY, url + "syntaxref2019.html#8820#9201",           // NOI18N
                        null, null, new TagAttributeInfo[] { new TagAttributeInfo("name", true, url + "syntaxref2019.html#15748#10919", false),       // NOI18N
                        new TagAttributeInfo("property", true, url + "syntaxref2019.html#10919#19482", false)}), // NOI18N
                new TagInfo("include", null, TagInfo.BODY_CONTENT_JSP, url + "syntaxref2020.html#8828#9228",              // NOI18N
                        null, null, new TagAttributeInfo[] { new TagAttributeInfo("flush", true, url + "syntaxref2020.html#17145#18376", false),       // NOI18N
                        new TagAttributeInfo("page", true, url + "syntaxref2020.html#10930#17145", true)}),       // NOI18N
                new TagInfo("param", null, TagInfo.BODY_CONTENT_EMPTY, url + "syntaxref2023.html#11538#11583",                // NOI18N
                        null, null, new TagAttributeInfo[] { new TagAttributeInfo("name", true, url + "syntaxref2023.html#11538#11583", false),        // NOI18N
                        new TagAttributeInfo("value", true, url + "syntaxref2023.html#11538#11583", true)}),      // NOI18N
                new TagInfo("params", null, TagInfo.BODY_CONTENT_JSP, url + "syntaxref2023.html#11538#11583",                 // NOI18N
                        null, null, new TagAttributeInfo[] {}),
                new TagInfo("plugin", null, TagInfo.BODY_CONTENT_JSP, url + "syntaxref2023.html#1004158#19029",               // NOI18N
                        null, null, new TagAttributeInfo[] { new TagAttributeInfo("align", false, url + "syntaxref2023.html#11516#11518", false),      // NOI18N
                        new TagAttributeInfo("archive", false, url + "syntaxref2023.html#11553#11516", false),    // NOI18N
                        new TagAttributeInfo("code", true, url + "syntaxref2023.html#11514#11515", false),        // NOI18N
                        new TagAttributeInfo("codebase", true, url + "syntaxref2023.html#11515#11547", false),   // NOI18N
                        new TagAttributeInfo("height", false, url + "syntaxref2023.html#11518#11568", false),     // NOI18N
                        new TagAttributeInfo("hspace", false, url + "syntaxref2023.html#11568#11520", false),     // NOI18N
                        new TagAttributeInfo("iepluginurl", false, url + "syntaxref2023.html#11526#11538", false),// NOI18N
                        new TagAttributeInfo("jreversion", false, url + "syntaxref2023.html#11520#11525", false), // NOI18N
                        new TagAttributeInfo("name", false, url + "syntaxref2023.html#11547#11553", false),       // NOI18N
                        new TagAttributeInfo("nspluginurl", false,url + "syntaxref2023.html#11525#11526", false),// NOI18N
                        new TagAttributeInfo("type", true, url + "syntaxref2023.html#10935#11514", false),        // NOI18N
                        new TagAttributeInfo("vspace", false, url + "syntaxref2023.html#11568#11520", false),     // NOI18N
                        new TagAttributeInfo("width", false, url + "syntaxref2023.html#11518#11568", false)}),    // NOI18N
                new TagInfo("setProperty", null, TagInfo.BODY_CONTENT_EMPTY, url + "syntaxref2025.html#8856#9329",           // NOI18N
                        null, null, new TagAttributeInfo[] { new TagAttributeInfo("name", true, url + "syntaxref2025.html#17612#1001786", true),        // NOI18N
                        new TagAttributeInfo("param", false, url + "syntaxref2025.html#9919#20483", false),      // NOI18N
                        new TagAttributeInfo("property", false, url + "syntaxref2025.html#1001786#9329", false),   // NOI18N
                        new TagAttributeInfo("value", false, url + "syntaxref2025.html#20483#9329", true)}),     // NOI18N
                new TagInfo("text", null, TagInfo.BODY_CONTENT_JSP, url + "syntaxref2026.html",
                        null, null, new TagAttributeInfo[]{}),
                new TagInfo("useBean", null, TagInfo.BODY_CONTENT_JSP, url + "syntaxref2027.html#8865#9359",     // NOI18N
                        null, null, new TagAttributeInfo[] { new TagAttributeInfo("beanName", false, url + "syntaxref2027.html#15804#9359", false),   // NOI18N
                        new TagAttributeInfo("class", false, url + "syntaxref2027.html#10968#19433", false),      // NOI18N
                        new TagAttributeInfo("id", true, url + "syntaxref2027.html#10964#10966", false),         // NOI18N
                        new TagAttributeInfo("scope", true, url + "syntaxref2027.html#10966#10968", false),      // NOI18N
                        new TagAttributeInfo("type", false, url + "syntaxref2027.html#19433#18019", false)}),     // NOI18N
                new TagInfo("declaration", null, TagInfo.BODY_CONTENT_JSP, url+"syntaxref204.html#10983#10991",                 // NOI18N
                        null, null, new TagAttributeInfo[] {}),
                new TagInfo("scriptlet", null, TagInfo.BODY_CONTENT_JSP, url+"syntaxref206.html#10996#11007",                 // NOI18N
                        null, null, new TagAttributeInfo[] {}),
                new TagInfo("directive.page", null, TagInfo.BODY_CONTENT_EMPTY, directiveJspData[1].getInfoString(),   // NOI18N
                        null, null, directiveJspData[1].getAttributes()),
                new TagInfo("directive.include", null, TagInfo.BODY_CONTENT_EMPTY, directiveJspData[1].getInfoString(),   // NOI18N
                        null, null, directiveJspData[0].getAttributes())
            };

            List<TagInfo> standardTagTagDatasList = new ArrayList<TagInfo>();
            standardTagTagDatasList.addAll(Arrays.asList(standardJspTagDatas));

            standardTagTagDatasList.add(new TagInfo("doBody", null, TagInfo.BODY_CONTENT_EMPTY, url + "syntaxref2017.html", null, null,            // NOI18N
                    new TagAttributeInfo[] { new TagAttributeInfo("scope", false, url + "syntaxref2017.html#1006246#syntaxref20.html", false),           // NOI18N
                    new TagAttributeInfo("var", false, url + "syntaxref2017.html#1006234#1006240", false),           // NOI18N
                    new TagAttributeInfo("varReader", false, url + "syntaxref2017.html#1006240#1006246", false)}));           // NOI18N

            standardTagTagDatasList.add(new TagInfo("invoke", null, TagInfo.BODY_CONTENT_JSP, url + "syntaxref2021.html#8837#1003634", null, null,            // NOI18N
                    new TagAttributeInfo[] { new TagAttributeInfo("fragment", true, url + "syntaxref2021.html#1007359#1007361", false),           // NOI18N
                    new TagAttributeInfo("scope", false, url + "syntaxref2021.html#1007373#1003634", false),           // NOI18N
                    new TagAttributeInfo("var", false, url + "syntaxref2021.html#1007361#1007367", false),           // NOI18N
                    new TagAttributeInfo("varReader", false, url + "syntaxref2021.html#1007367#1007373", false)}));           // NOI18N

            standardTagTagDatasList.add(new TagInfo("directive.tag", null, TagInfo.BODY_CONTENT_EMPTY, directiveTagFileData[2].getInfoString(), // NOI18N
                        null, null, directiveTagFileData[2].getAttributes()));
            standardTagTagDatasList.add(new TagInfo("directive.attribute", null, TagInfo.BODY_CONTENT_EMPTY, directiveTagFileData[0].getInfoString(), // NOI18N
                        null, null, directiveTagFileData[0].getAttributes()));
            standardTagTagDatasList.add(new TagInfo("directive.variable", null, TagInfo.BODY_CONTENT_EMPTY, directiveTagFileData[4].getInfoString(), // NOI18N
                        null, null, directiveTagFileData[4].getAttributes()));

            standardTagTagDatas = standardTagTagDatasList.toArray(new TagInfo[]{});

        }

        if (xmlJspTagDatas == null) {
            TagInfo[] commonXMLTagDatas;
            commonXMLTagDatas = new TagInfo[]{
                        new TagInfo("output", null, TagInfo.BODY_CONTENT_JSP, url + "syntaxref2022.html#1004130#1007521",                 // NOI18N
                        null, null, new TagAttributeInfo[] {new TagAttributeInfo("doctype-public", false, "url + syntaxref2022.html#1007534#1007521", false),  // NOI18N
                        new TagAttributeInfo("doctype-root-element", false, "url + syntaxref2022.html#1007528#1007532", false),    // NOI18N
                        new TagAttributeInfo("doctype-system", false, url + "syntaxref2022.html#1007532#1007534", false),  // NOI18N
                        new TagAttributeInfo("omit-xml-declaration", false, url + "syntaxref2022.html#1007525#1007528"   , false)}),    // NOI18N
                        new TagInfo("root", null, TagInfo.BODY_CONTENT_JSP, url+"syntaxref2024.html#1003283#1003311",                         // NOI18N
                        null, null, new TagAttributeInfo[] {new TagAttributeInfo("version", false, url+"syntaxref2024.html#1003299#1003301", false),
                        new TagAttributeInfo("xmlns:jsp", false, url+"syntaxref2024.html#1003297#1003299", false),
                        new TagAttributeInfo("xmlns:x", false, url+"syntaxref2024.html#1003301#1003311", false)})
            };

            List<TagInfo> xmlJspTagDatasList = new ArrayList<TagInfo>();
            xmlJspTagDatasList.addAll(Arrays.asList(standardJspTagDatas));
            xmlJspTagDatasList.addAll(Arrays.asList(commonXMLTagDatas));
            xmlJspTagDatas = xmlJspTagDatasList.toArray(new TagInfo[]{});

            List<TagInfo> xmlTagFileTagDatasList = new ArrayList<TagInfo>();
            xmlTagFileTagDatasList.addAll(Arrays.asList(standardTagTagDatas));
            xmlTagFileTagDatasList.addAll(Arrays.asList(commonXMLTagDatas));
            xmlTagFileTagDatas = xmlTagFileTagDatasList.toArray(new TagInfo[]{});

        }


    }

    private TagInfo[] getTagInfos(){
        TagInfo[] rValue;
        if (fobj != null &&
                ("text/x-jsp".equals(fobj.getMIMEType()) ||
                "text/x-tag".equals(fobj.getMIMEType()))){
            if ( isXmlSyntax()){
                if (NbEditorUtilities.getMimeType(getDocument()).equals(JspUtils.TAG_MIME_TYPE))
                    rValue = xmlTagFileTagDatas;
                else
                    rValue = xmlJspTagDatas;
            } else
                if (NbEditorUtilities.getMimeType(getDocument()).equals(JspUtils.TAG_MIME_TYPE))
                    rValue = standardTagTagDatas;
                else
                    rValue = standardJspTagDatas;
        } else
            rValue = new TagInfo[0];
        return rValue;
    }

    @Override
    public String toString() {
        return printJspCompletionInfo();
    }

    /** Debug output of all tags and directives. */
    private String printJspCompletionInfo() {
        StringBuffer output = new StringBuffer();

        output.append("TAGS\n");    // NOI18N
        List tagPrefixes = getTagPrefixes("");  // NOI18N
        for (int i = 0; i < tagPrefixes.size(); i++) {
            String prefix = (String)tagPrefixes.get(i);
            output.append("  " + prefix + "\n");    // NOI18N
            List tags = getTags(prefix, "");    // NOI18N
            for (int j = 0; j < tags.size(); j++) {
                if (tags.get(j) instanceof TagInfo){
                    TagInfo ti = (TagInfo) tags.get(j);
                    output.append("    " + ti.getTagName() + "\n");
                    TagAttributeInfo[] attributes =  ti.getAttributes();
                    for (int k = 0; k < attributes.length; k++) {
                        output.append("      " + attributes[k].getName() + "\n");// NOI18N
                    }
                } else {
                    String tagName = (String)tags.get(j);
                    output.append("    " + tagName + "\n"); // NOI18N
                    List attributes = getTagAttributes(prefix, tagName, "");// NOI18N
                    for (int k = 0; k < attributes.size(); k++) {
                        String attribute = (String)attributes.get(k);
                        output.append("      " + attribute + "\n");// NOI18N
                    }
                }
            }

        }

        output.append("DIRECTIVES\n");// NOI18N
        List directives = getDirectives("");// NOI18N
        for (int i = 0; i < directives.size(); i++) {
            if (directives.get(i) instanceof TagInfo){
                TagInfo ti = (TagInfo) directives.get(i);
                output.append("    " + ti.getTagName() + "\n");
                TagAttributeInfo[] attributes =  ti.getAttributes();
                for (int k = 0; k < attributes.length; k++) {
                    output.append("      " + attributes[k].getName() + "\n");// NOI18N
                }
            } else {
                String directive = (String)directives.get(i);
                output.append("  " + directive + "\n");// NOI18N
                List attributes = getDirectiveAttributes(directive, "");// NOI18N
                for (int k = 0; k < attributes.size(); k++) {
                    String attribute = (String)attributes.get(k);
                    output.append("      " + attribute + "\n");// NOI18N
                }
            }
        }

        return output.toString();
    }

    /** Returns an item on offset <code>offset</code>
     * This method is largely a workaround for a bug in getTokenChain().
     * If offset falls right between two items, returns one which is just before
     * offset. If <code>offset == 0</code>, retruns null. */
    @Deprecated()
    public TokenItem getItemAtOrBefore(int offset) throws BadLocationException {
        TokenItem backItem = null;
        int chainLength = 100;
        while (backItem == null) {
            if (offset < getDocument().getLength()) {
                backItem = getTokenChain( offset,
                        Math.min(offset + chainLength, getDocument().getLength())/*, false*/ );
            } else {
                // @ end of document
                backItem = getTokenChain(Math.max(offset-50, 0), offset);
            }

            if (chainLength++ > 1000)
                break;
        }
        if (backItem == null)
            return null;

        // forward to the offset where our token definitely is
        TokenItem item;
        while (true) {
            item = backItem.getNext();
            //System.out.println("looking at item " + item);
            if (item == null) {
                item = backItem;
                break;
            }
            if (item.getOffset() > offset) {
                item = backItem;
                break;
            }
            backItem = item;
        }

        TokenItem adjustedItem = (item.getOffset() == offset) ?
            item.getPrevious() : item;

        return adjustedItem;
    }

    /** Returns SyntaxElement instance for block of tokens, which is either
     * surrounding given offset, or is just after the offset.
     * @param offset offset in document where to search for SyntaxElement
     * @return SyntaxElement surrounding or laying before the offset
     */
    public SyntaxElement getElementChain( int offset ) throws BadLocationException {
        TokenItem item = getItemAtOrBefore(offset);
        if (item == null)
            return null;
        TokenID id = item.getTokenID();

        if (/*id == JspTagTokenContext.COMMENT || */
                id == JspTagTokenContext.ERROR ||
                id == JspTagTokenContext.TEXT ||
                id == JspMultiTokenContext.ERROR ||
                /*id == JspDirectiveTokenContext.COMMENT || */
                id == JspDirectiveTokenContext.ERROR ||
                id == JspDirectiveTokenContext.TEXT
                ) {
            return null;
        }

        //JSP comment
        if(id == JspTagTokenContext.COMMENT || id == JspDirectiveTokenContext.COMMENT) {
            return getCommentChain(item, offset);
        }

        //Expression language handling
        if(item.getTokenContextPath().contains(ELTokenContext.contextPath)) {
            return getELChain(item, offset);
        }

        if (id == JspTagTokenContext.SYMBOL2 || id == JspDirectiveTokenContext.SYMBOL2) {
            if (isScriptStartToken(item)) {
                return getScriptingChain(item, offset);
            }

            if ((getTokenEnd(item) == offset) && isScriptEndToken(item)) {
                item.getNext();
                if (!isTagDirToken(item))
                    return getContentChain(item, offset);
            }
            return null;
        }

        if (id == JspTagTokenContext.TAG ||
                id == JspTagTokenContext.SYMBOL ||
                id == JspTagTokenContext.ATTRIBUTE ||
                id == JspTagTokenContext.ATTR_VALUE ||
                id == JspTagTokenContext.WHITESPACE ||
                id == JspTagTokenContext.EOL ||
                id == JspDirectiveTokenContext.TAG ||
                id == JspDirectiveTokenContext.SYMBOL ||
                id == JspDirectiveTokenContext.ATTRIBUTE ||
                id == JspDirectiveTokenContext.ATTR_VALUE ||
                id == JspDirectiveTokenContext.WHITESPACE ||
                id == JspDirectiveTokenContext.EOL) {
            // may be intetesting: tag, directive,
            // but may also be a comment. Look back for SYMBOL: <, </, <%@
            // or COMMENT
            TokenItem elementStart = item;
            do {
                //System.out.println("backtracking, elementStart = " + elementStart);
                if (elementStart == null)
                    return null;
                if (elementStart.getTokenID() == JspTagTokenContext.SYMBOL
                        || elementStart.getTokenID() == JspDirectiveTokenContext.SYMBOL) {
                    if (elementStart.getImage().equals("<")) {   // NOI18N
                        return getTagOrDirectiveChain(true, elementStart, offset);
                    }
                    if (elementStart.getImage().equals("</")) {   // NOI18N
                        return getEndTagChain(elementStart, offset);
                    }
                    if (elementStart.getImage().equals("<%@")) {   // NOI18N
                        return getTagOrDirectiveChain(false, elementStart, offset);
                    }
                }
                if(elementStart.getTokenID() == JspTagTokenContext.ERROR
                        || elementStart.getTokenID() == JspDirectiveTokenContext.ERROR) {
                    //an error in JSP code
                    return null;
                }
                if (elementStart.getTokenID() == JspTagTokenContext.COMMENT
                        || elementStart.getTokenID() == JspDirectiveTokenContext.COMMENT) {
                    return null;
                }
                elementStart = elementStart.getPrevious();
            }
            while (true);
        }

        // now we are either in the scripting language or in the content language.
        // to determine which one it is, look back for SYMBOL2: <%, <%=, <%!
        // (scripting language) or for any other JspTag token (content language).
        // if nothing found, we are in the content language
        if (isScriptingOrContentToken(item)) {
            TokenItem elementStart = item;
            do {
                if (elementStart.getPrevious() == null) {
                    // we backtracked to the beginning without finding
                    // a distinguishing symbol - we are in the content language
                    return getContentChain(elementStart, offset);
                }
                elementStart = elementStart.getPrevious(); // now non-null
                if (!isScriptingOrContentToken(elementStart)
                        || elementStart.getTokenID() == JspTagTokenContext.COMMENT
                        || elementStart.getTokenID() == JspDirectiveTokenContext.COMMENT
                        || elementStart.getTokenContextPath().contains(ELTokenContext.contextPath)) {
                    // something from JSP
                    if (isScriptStartToken(elementStart)) {
                        return getScriptingChain(elementStart.getNext(), offset);
                    } else {
                        return getContentChain(elementStart.getNext(), offset);
                    }
                }
            }
            while (true);
        }

        return null;
    }

    /** Returns true if item is a starting symbol for a block in
     * the scripting language. */
    private boolean isScriptStartToken(TokenItem item) {
        if (item == null)
            return false;
        TokenID id = item.getTokenID();
        if (id == JspTagTokenContext.SYMBOL2
                || id == JspDirectiveTokenContext.SYMBOL2) {
            String image = item.getImage();
            if (image.equals("<%") ||   // NOI18N
                    image.equals("<%=") ||  // NOI18N
                    image.equals("<%!"))    // NOI18N
                return true;
        }
        return false;
    }

    /** Returns true if item is an ending symbol for a block in
     * the scripting language. */
    private boolean isScriptEndToken(TokenItem item) {
        if (item == null)
            return false;
        TokenID id = item.getTokenID();
        if (id == JspTagTokenContext.SYMBOL2
                || id == JspDirectiveTokenContext.SYMBOL2) {
            String image = item.getImage();
            if (image.equals("%>")) // NOI18N
                return true;
        }
        return false;
    }

    /** Returns true if item is an item which can be INSIDE
     * a JSP tag or directive (i.e. excuding delimeters). */
    private boolean isInnerTagDirToken(TokenItem item) {
        if (!isTagDirToken(item))
            return false;
        TokenID id = item.getTokenID();
        if (id == JspTagTokenContext.SYMBOL
                || id == JspDirectiveTokenContext.SYMBOL) {
            String image = item.getImage();
            if (image.equals("<") ||    // NOI18N
                    image.equals("</") ||   // NOI18N
                    image.equals("<%@") ||  // NOI18N
                    image.equals("%>") ||   // NOI18N
                    image.equals(">") ||    // NOI18N
                    image.equals("/>"))     // NOI18N
                return false;
        }
        return true;
    }

    /** Returns true if item is an item which can be INSIDE
     * a JSP tag or directive (i.e. excuding delimeters). */
    private boolean isTagDirToken(TokenItem item) {
        if (item == null)
            return false;
        TokenID id = item.getTokenID();
        if (id == null)
            return false;
        if ((id != JspTagTokenContext.TEXT) &&
                (id != JspTagTokenContext.ERROR) &&
                (id != JspTagTokenContext.TAG) &&
                (id != JspTagTokenContext.SYMBOL) &&
                (id != JspTagTokenContext.ATTRIBUTE) &&
                (id != JspTagTokenContext.ATTR_VALUE) &&
                (id != JspTagTokenContext.WHITESPACE) &&
                (id != JspTagTokenContext.EOL) &&
                (id != JspDirectiveTokenContext.TEXT) &&
                (id != JspDirectiveTokenContext.ERROR) &&
                (id != JspDirectiveTokenContext.TAG) &&
                (id != JspDirectiveTokenContext.SYMBOL) &&
                (id != JspDirectiveTokenContext.ATTRIBUTE) &&
                (id != JspDirectiveTokenContext.ATTR_VALUE) &&
                (id != JspDirectiveTokenContext.WHITESPACE) &&
                (id != JspDirectiveTokenContext.EOL )) {
            return false;
        }
        // PENDING - EOL can still be a comment
        return true;
    }

    /** Return true if this item does not belong to JSP syntax
     *  and belongs to one of the syntaxes we delegate to. */
    private boolean isScriptingOrContentToken(TokenItem item) {
        if (item == null)
            return true;
        TokenID id = item.getTokenID();
        if (id == null)
            return true;
        if ((id == JspTagTokenContext.TEXT) ||
                (id == JspTagTokenContext.ERROR) ||
                (id == JspTagTokenContext.TAG) ||
                (id == JspTagTokenContext.SYMBOL) ||
                (id == JspTagTokenContext.ATTRIBUTE) ||
                (id == JspTagTokenContext.ATTR_VALUE) ||
                (id == JspTagTokenContext.SYMBOL2) ||
                (id == JspTagTokenContext.EOL) ||
                (id == JspDirectiveTokenContext.TEXT) ||
                (id == JspDirectiveTokenContext.ERROR) ||
                (id == JspDirectiveTokenContext.TAG) ||
                (id == JspDirectiveTokenContext.SYMBOL) ||
                (id == JspDirectiveTokenContext.ATTRIBUTE) ||
                (id == JspDirectiveTokenContext.ATTR_VALUE) ||
                (id == JspDirectiveTokenContext.SYMBOL2) ||
                (id == JspDirectiveTokenContext.EOL) ||
                (id == JspMultiTokenContext.ERROR))
            return false;
        return true;
    }

    public boolean isValueBeginning(String text) {
        if (text.trim().endsWith("\"\""))   // NOI18N
            return false;
        for (int i = 0; i < text.length(); i++) {
            char c = text.charAt(i);
            if ((c != ' ') &&
                    (c != '=') &&
                    (c != '"'))
                return false;
        }
        return true;
    }

    // ------- METHODS FOR CONSTRUCTING SEMANTICALLY LIKNKED CHAINS OF TOKENS ------

    private SyntaxElement getCommentChain(TokenItem token, int offset) {
        //we are somewhere in a JSP comment - need to find its start and end
        //backtrace for start
        TokenItem start = null;
        TokenItem search = token;
        do {
            if(search != null && search.getImage().startsWith("<%--")) { //NOI18N
                start = search;
                break;
            }
            search = search.getPrevious();
        } while(search != null);

        if(start == null) {
            //didn't find a comment start - strange???
            return null;
        }

        //find comment end
        TokenItem end = null;
        TokenItem prevNonNullToken = token;
        search = token;
        do {
            if(search != null && search.getImage().endsWith("--%>")) { //NOI18N
                end = search;
                break;
            }
            prevNonNullToken = search;
            search = search.getNext();
        } while(search != null);
        if(end == null) {
            //comment to the end of the file - may happed
            end = prevNonNullToken; //last non-null token
        }

        return new SyntaxElement.Comment(this, start.getOffset(), getTokenEnd(end));
    }

    private SyntaxElement getELChain(TokenItem token, int offset) {
        //we are somewhere in an expression language - need to find its start and end
        //backtrace for start
        TokenItem scan = token;
        TokenItem start = null;
        do {
            start = scan;
            scan = scan.getPrevious();
        } while(scan != null && scan.getTokenContextPath().contains(ELTokenContext.contextPath));

        //find comment end
        TokenItem end = null;
        scan = token;
        do {
            end = scan;
            scan = scan.getNext();
        } while(scan != null && scan.getTokenContextPath().contains(ELTokenContext.contextPath));

        return new SyntaxElement.ExpressionLanguage(this, start.getOffset(), getTokenEnd(end));
    }


    /** Gets an element representing a tag or directive starting with token item firstToken. */
    private SyntaxElement getTagOrDirectiveChain(boolean tag, TokenItem firstToken, int offset) {
        //suppose we are in a tag or directive => we do not have to distinguish tokenIDs -
        //it is sufficient to work with numeric ids (solves directive/tag tokenID problem)
        TokenItem item = firstToken.getNext();
        String name = getWholeWord(item, JspTagTokenContext.TAG);
        while ((item != null) && (item.getTokenID().getNumericID() == JspTagTokenContext.TAG_ID ||
                item.getTokenID().getNumericID() == JspTagTokenContext.WHITESPACE_ID ))
            item = item.getNext();
        TreeMap attributes = new TreeMap();
        while (isInnerTagDirToken(item)) {
            // collect the attributes
            if (item.getTokenID().getNumericID() == JspTagTokenContext.ATTRIBUTE_ID) {
                String attributeName = getWholeWord(item, JspTagTokenContext.ATTRIBUTE);
                // forward to the next non-ATTRIBUTE token
                while ((item != null) && (item.getTokenID().getNumericID() == JspTagTokenContext.ATTRIBUTE_ID))
                    item = item.getNext();
                // find the value
                while ((item != null) &&
                        (item.getTokenID().getNumericID() == JspTagTokenContext.SYMBOL_ID) &&
                        (isValueBeginning(item.getImage())))
                    item = item.getNext();
                StringBuffer value = new StringBuffer();
                while ((item != null)
                        && ((item.getTokenID().getNumericID() == JspTagTokenContext.ATTR_VALUE_ID)
                        || (item.getTokenID().getNumericID() == JspTagTokenContext.EOL_ID))) {
                    value.append(item.getImage());
                    item = item.getNext();
                    // request time values
                    if ((item != null) && (item.getTokenID().getNumericID() == JspTagTokenContext.SYMBOL2_ID)) {
                        // scripting language - something like request time value of a JSP tag
                        while (!isScriptEndToken(item)) {
                            if (item == null)
                                break;
                            else {
                                value.append(item.getImage());
                                item = item.getNext();
                            }
                        }
                        // now it's a script end token
                        if (item != null) {
                            value.append(item.getImage());
                            item = item.getNext();
                        }
                    }

                    //expression language - just put its content into attribute value
                    if((item != null) && (item.getTokenContextPath().contains(ELTokenContext.contextPath))) {
                        //go over all EL tokens and add them to the attribute value
                        while(item.getTokenContextPath().contains(ELTokenContext.contextPath)) {
                            item = item.getNext();
                            if(item == null) break;
                            value.append(item.getImage());
                        }
                    }

                }
                String vString = value.toString();
                // cut off the beginning and ending quotes
                if (vString.startsWith("\""))   // NOI18N
                    vString = vString.substring(1);
                if (vString.endsWith("\""))     // NOI18N
                    vString = vString.substring(0, vString.length() - 1);
                attributes.put(attributeName, vString);
                continue;
            }
            if (item.getTokenID().getNumericID() == JspTagTokenContext.SYMBOL2_ID) {
                // scripting language - something like request time value of a JSP tag
                while (!isScriptEndToken(item)) {
                    if (item == null)
                        break;
                    else
                        item = item.getNext();
                }
                // now it's a script end token
                if (item != null)
                    item = item.getNext();
                continue;
            }
            // a token I am not interested in
            item = item.getNext();
        }
        if (tag) {
            boolean endslash= false;
            if (item != null)
                endslash = (item.getImage().equals("/>"));   // NOI18N

            return new SyntaxElement.Tag(this, firstToken.getOffset(),
                    (item != null)? getTokenEnd(item): getDocument().getLength(),
                    name, attributes, endslash);
        } else {
            return new SyntaxElement.Directive(this, firstToken.getOffset(),
                    (item != null)? getTokenEnd(item): getDocument().getLength(),
                    name, attributes);
        }
    }

    private SyntaxElement getEndTagChain(TokenItem firstToken, int offset) {
        TokenItem item = firstToken.getNext();
        String name = getWholeWord(item, JspTagTokenContext.TAG);
        while ((item != null) && (item.getTokenID() == JspTagTokenContext.TAG))
            item = item.getNext();
        while (isInnerTagDirToken(item)) {
            item = item.getNext();
        }
        return new SyntaxElement.EndTag(this, firstToken.getOffset(),
                getTokenEnd(item), name);
    }

    private String getWholeWord(TokenItem firstToken, TokenID requestedTokenID) {
        StringBuffer sb = new StringBuffer();
        while ((firstToken != null) && (firstToken.getTokenID().getNumericID() == requestedTokenID.getNumericID())) {
            sb.append(firstToken.getImage());
            firstToken = firstToken.getNext();
        }
        return sb.toString().trim();
    }

    /** Returns an element of scripting language starting with firstToken.
     * If forstToken is null, returns element representing end of the document.
     */
    private SyntaxElement getScriptingChain(TokenItem firstToken, int offset) {
        if (firstToken == null) {
            return new SyntaxElement.ScriptingL(this,
                    getDocument().getLength(), getDocument().getLength());
        }
        TokenItem item = firstToken;
        do {
            TokenItem nextItem = item.getNext();
            if (nextItem == null) {
                return new SyntaxElement.ScriptingL(this,
                        firstToken.getOffset(), getDocument().getLength());
            }
            if (!isScriptingOrContentToken(nextItem))
                return new SyntaxElement.ScriptingL(this,
                        firstToken.getOffset(), getTokenEnd(nextItem));
            item = nextItem;
        }
        while (true);
    }

    /** Returns an element of content language starting with firstToken.
     * If forstToken is null, returns element representing end of the document.
     */
    private SyntaxElement getContentChain(TokenItem firstToken, int offset) {
        if (firstToken == null) {
            return new SyntaxElement.ContentL(this,
                    getDocument().getLength(), getDocument().getLength());
        }
        TokenItem item = firstToken;
        do {
            TokenItem nextItem = item.getNext();
            if (nextItem == null) {
                return new SyntaxElement.ContentL(this,
                        firstToken.getOffset(), getDocument().getLength());
            }
            if (!isScriptingOrContentToken(nextItem)
                    || nextItem.getTokenID() == JspTagTokenContext.COMMENT
                    || nextItem.getTokenID() == JspDirectiveTokenContext.COMMENT
                    || nextItem.getTokenContextPath().contains(ELTokenContext.contextPath))
                return new SyntaxElement.ContentL(this,
                        firstToken.getOffset(), getTokenEnd(item));
            item = nextItem;
        }
        while (true);
    }

    /** The way how to get previous SyntaxElement in document. It is not intended
     * for direct usage, and thus is not public. Usually, it is called from
     * SyntaxElement's method getPrevious()
     */
    SyntaxElement getPreviousElement( int offset ) throws BadLocationException {
        if (offset == 0)
            return null;
        SyntaxElement elem = null;
        offset--;
        do {
            elem = getElementChain( offset);
            if (elem == null){
                TokenItem ti = getItemAtOrBefore(offset);
                if (ti == null)
                    return null;
                offset = ti.getOffset() -1 ;
            }
        } while (elem == null && offset >= 0);
        return elem;
    }

    SyntaxElement getNextElement(int offset) throws BadLocationException {
        int doclen = getDocument().getLength();
        if(offset >= doclen)
            return null;

        SyntaxElement elem = null;
        offset++;
        do {
            elem = getElementChain(offset);
            if(elem == null) {
                TokenItem ti = getItemAtOrBefore(offset);
                if(ti == null)
                    return null;
                offset = getTokenEnd(ti) + 1;
            }
        } while(elem == null && offset < doclen);

        return elem;
    }

    public List<JspCompletionItem> getPossibleEndTags(int offset, int anchor, String pattern) throws BadLocationException {
        return getPossibleEndTags(offset, anchor, pattern, false); //return all end tags
    }

    public List<JspCompletionItem> getPossibleEndTags(int offset, int anchor, String pattern, boolean firstOnly) throws BadLocationException {
        SyntaxElement elem = getElementChain( offset );

        Stack<String> stack = new Stack<String>();
        List<JspCompletionItem> result = new ArrayList<JspCompletionItem>();
        Set<String> found = new HashSet<String>();

        if( elem != null ) {
            elem = elem.getPrevious();  // we need smtg. before our </
        } else {    // End of Document
            if( offset > 0 ) {
                elem = getElementChain( offset-1 );
            } else { // beginning of document too, not much we can do on empty doc
                return result;
            }
        }


        for( ; elem != null; elem = elem.getPrevious() ) {

            if( elem instanceof SyntaxElement.EndTag ) {
                stack.push( ((SyntaxElement.EndTag)elem).getName() );
            } else if( elem instanceof SyntaxElement.Tag ) {
                SyntaxElement.Tag tag = (SyntaxElement.Tag)elem;

                if (tag.isClosed())
                    continue;

                String image = tag.getName();
                String prefix = image.substring(0, image.indexOf(':'));
                String name = image.substring(image.indexOf(':')+1);
                TagInfo ti = null;

                TagLibraryInfo tli = getTagLibrary(prefix, true);
                if (tli != null) {
                    ti = tli.getTag(name);
                    if (ti == null) {
                        // try to find the info among tag files
                        TagFileInfo tagFileInfo = tli.getTagFile(name);
                        if (tagFileInfo != null) {
                            ti = tagFileInfo.getTagInfo();
                        }
                    }
                }

                if (STANDARD_JSP_PREFIX.equals(prefix)) {
                    initCompletionData();
                    TagInfo[] stanTagDatas = getTagInfos();
                    for (int i=0; i<stanTagDatas.length; i++) {
                        if (stanTagDatas[i].getTagName().equals(name)) {
                            ti = stanTagDatas[i];
                            break;
                        }
                    }
                }

                if (ti == null) continue; // Unknown tag - ignore

                if( stack.empty() ) {           // empty stack - we are on the same tree deepnes - can close this tag
                    if( image.startsWith( pattern ) && !found.contains( image ) ) {    // add only new items
                        found.add( image );

                        if (ti.getBodyContent().equalsIgnoreCase(TagInfo.BODY_CONTENT_EMPTY))
                            continue;

                        result.add(JspCompletionItem.createTag("/"+image, anchor, ti ) );  // NOI18N

                        if(firstOnly) break; //return only the first found not-finished start token
                    }
                    // if( ! tag.hasOptionalEnd() ) break;  // If this tag have required EndTag, we can't go higher until completing this tag
                } else {                        // not empty - we match content of stack
                    if( stack.peek().equals( image ) ) { // match - close this branch of document tree
                        stack.pop();
                    } // else if( ! tag.hasOptionalEnd() ) break; // we reached error in document structure, give up
                }

                // this is error - end of empty tag
                if (ti.getBodyContent().equalsIgnoreCase(TagInfo.BODY_CONTENT_EMPTY))
                    continue;

                // if( tag.isEmpty() ) continue; // ignore empty Tags - they are like start and imediate end

            }
        }

        return result;
    }

    public CompletionItem getAutocompletedEndTag(int offset) {
        try {
            SyntaxElement elem = getElementChain( offset - 1);
            if(elem instanceof SyntaxElement.Tag) {
                String tagName = ((SyntaxElement.Tag)elem).getName();
                return HtmlCompletionItem.createAutocompleteEndTag(tagName, offset);
            }
        }catch(BadLocationException e) {
            Logger.getLogger("global").log(Level.INFO, null, e);
        }
        return null;
    }

    public FileObject getFileObject() {
        return fobj;
    }

    /** Get the bracket finder that will search for the matching bracket
     * or null if the bracket character doesn't belong to bracket
     * characters.
     *
     * Customized finder recognizes also '<' and '>' as bracket chars. It is set to be used
     * in findMatchingBlock.
     */
    @Override
    protected ExtSyntaxSupport.BracketFinder getMatchingBracketFinder(char bracketChar) {
        if (useCustomBracketFinder) {
            JspSyntaxSupport.BracketFinder bf = new JspSyntaxSupport.BracketFinder(bracketChar);
            return bf.isValid()? bf: null;
        } else{
            return super.getMatchingBracketFinder(bracketChar);
        }
    }

    /** Find matching bracket or more generally block
     * that matches with the current position.
     * @param offset position of the starting bracket
     * @param simple whether the search should skip comment and possibly other areas.
     *  This can be useful when the speed is critical, because the simple
     *  search is faster.
     * @return array of integers containing starting and ending position
     *  of the block in the document. Null is returned if there's
     *  no matching block.
     */
    @Override
    public int[] findMatchingBlock(int offset, boolean simpleSearch)
            throws BadLocationException {

        int [] r_value = null;

        TokenItem token = getItemAtOrBefore((offset<getDocument().getLength())?offset+1:offset);
        if (token != null){
            //do we need to match jsp comment?
            if (token.getTokenID() == JspTagTokenContext.COMMENT) {
                return findMatchingJspComment(token, offset);
            }
            // Is it matching of scriptlet delimiters?
            if (token.getTokenContextPath().contains(JspTagTokenContext.contextPath)
                    && token.getTokenID().getNumericID() == JspTagTokenContext.SYMBOL2_ID){
                return findMatchingScripletDelimiter(token);
            }
            // Try to match the tags.
            if (token.getTokenContextPath().contains(JspTagTokenContext.contextPath)){
                return findMatchingTag(token);
            } else {
                if (isScriptingOrContentToken(token)) {
                    useCustomBracketFinder = false;
                } else {
                    useCustomBracketFinder = true;
                }
                r_value =  super.findMatchingBlock(offset, simpleSearch);
            }
        }


        return r_value;
    }

    private int[] findMatchingJspComment(TokenItem token, int offset) {
        String tokenImage = token.getImage();
        if(tokenImage.startsWith("<%--") && (offset < (token.getOffset()) + "<%--".length())) { //NOI18N
            //start html token - we need to find the end token of the html comment
            while(token != null) {
                if((token.getTokenID() == JspTagTokenContext.COMMENT)
                        || (token.getTokenID() == JspTagTokenContext.EOL)) {
                    if(token.getImage().endsWith("--%>")) { //NOI18N
                        //found end token
                        int start = token.getOffset() + token.getImage().length() - "--%>".length(); //NOI18N
                        int end = token.getOffset() + token.getImage().length();
                        return new int[] {start, end};
                    }
                } else break;
                token = token.getNext();
            }
        } else if(tokenImage.endsWith("--%>") && (offset >= (token.getOffset()) + token.getImage().length() - "--%>".length())) { //NOI18N
            //end html token - we need to find the start token of the html comment
            while(token != null) {
                if((token.getTokenID() == JspTagTokenContext.COMMENT)
                        || (token.getTokenID() == JspTagTokenContext.EOL)) {
                    if(token.getImage().startsWith("<%--")) { //NOI18N
                        //found end token
                        int start = token.getOffset();
                        int end = token.getOffset() + "<%--".length(); //NOI18N
                        return new int[] {start, end};
                    }
                } else break;
                token = token.getPrevious();
            }
        }
        return null;
    }

    private int [] findMatchingScripletDelimiter(TokenItem token){
        if (token.getImage().charAt(0) == '<'){
            do{
                token = token.getNext();
            } while (token != null
                    && !(token.getTokenContextPath().contains(JspTagTokenContext.contextPath)
                    &&token.getTokenID().getNumericID() == JspTagTokenContext.SYMBOL2_ID));
        } else {
            do{
                token = token.getPrevious();
            } while (token != null
                    && !(token.getTokenContextPath().contains(JspTagTokenContext.contextPath)
                    &&token.getTokenID().getNumericID() == JspTagTokenContext.SYMBOL2_ID));
        }
        if (token != null){
            return new int [] {token.getOffset(), token.getOffset() + token.getImage().length()};
        }
        return null;
    }

    private int[] findMatchingTag(TokenItem token){
        // TODO - replanning to the other thread. Now it's in awt thread
        // if the curret is after jsp tag ( after the char '>' ), ship inside the tag
        if (token != null && token.getTokenID().getNumericID() == JspTagTokenContext.SYMBOL_ID
                && token.getImage().charAt(token.getImage().length()-1) == '>')
            token = token.getPrevious();
        boolean isInside = false;  // flag, whether the curret is somewhere in a jsp tag
        if (token != null
                && ((token.getTokenID().getNumericID() == JspTagTokenContext.TAG_ID && token.getImage().trim().length() > 0)
                || (token.getTokenID().getNumericID() == JspTagTokenContext.SYMBOL_ID && token.getImage().charAt(0)=='<'))){
            if (token.getTokenID().getNumericID() == JspTagTokenContext.SYMBOL_ID) // the starting of the jsp tag
                // we are somewhere at beginning of the jsp tag. Find out the token with the jsp tag.
                while (token !=null
                && !(token.getTokenID().getNumericID() == JspTagTokenContext.TAG_ID
                && token.getImage().trim().length() > 0))
                    token = token.getNext();    // move at the jsp tag
            isInside = true; // the curret is somewhere in '<jsptag' or '</jsptag'
        } else {
            // find out whether the curret is inside a jsp tag
            if (token != null
                    && !(token.getTokenID().getNumericID() == JspTagTokenContext.TAG_ID && token.getImage().trim().length() > 0)){
                token = token.getPrevious();
                //try to find the beginning of the tag.
                while (token!=null
                        && !(token.getTokenID().getNumericID() == JspTagTokenContext.TAG_ID
                        && token.getImage().trim().length() > 0)  // this is hack, because a whitspaces are returned are with TAG_ID
                        && !(token.getTokenID().getNumericID() == JspTagTokenContext.SYMBOL_ID
                        && token.getImage().charAt(token.getImage().length()-1) == '>'))
                    token = token.getPrevious();
                if (token!=null && token.getTokenID().getNumericID() == JspTagTokenContext.TAG_ID)
                    isInside = true;
            }
        }
        // Now we have the begining of the tag and we can start with the finding opposit tag.
        if (token != null && token.getTokenID().getNumericID() == JspTagTokenContext.TAG_ID && isInside){

            int start; // possition where the matched tag starts
            int end;   // possition where the matched tag ends
            int poss = 0; // how many the same tags is inside the mathed tag
            boolean singleTag = false;
            String tag = token.getImage().trim();

            while (token != null && token.getTokenID().getNumericID() != JspTagTokenContext.SYMBOL_ID) {
                token = token.getPrevious();
            }
            if (token == null)
                return null;
            if ((token.getImage().length() == 2) && token.getImage().charAt(1) == '/'){
                while ( token != null){
                    // see issue #229149
                    if (token.getTokenID().getNumericID() == JspTagTokenContext.SYMBOL_ID) {
                        if ("/>".equals(token.getImage())) { //NOI18N
                            singleTag = true;
                        } else if ("<".equals(token.getImage())) { //NOI18N
                            singleTag = false;
                        }
                    }
                    if (token.getTokenID().getNumericID() == JspTagTokenContext.TAG_ID) {
                        if (token.getImage().trim().equals(tag) && !singleTag){
                            while (token != null && token.getTokenID().getNumericID() != JspTagTokenContext.SYMBOL_ID) {
                                token = token.getPrevious();
                            }
                            if (token != null) {
                                if (token.getImage().length() == 1){
                                    if (poss == 0){
                                        start = token.getOffset();
                                        token = token.getNext();
                                        end = token.getOffset()+token.getImage().length();

                                        // add everything up to ending > or /> into the selection
                                        TokenItem next = token.getNext();
                                        while (next != null &&
                                                !((next.getTokenID() == JspTagTokenContext.SYMBOL && next.getImage().endsWith(">"))
                                                || next.getTokenID() == JspTagTokenContext.EOL)) {
                                            end += next.getImage().length();
                                            next = next.getNext();
                                        }
                                        if (next != null && next.getTokenID() != JspTagTokenContext.EOL) {
                                            end += next.getImage().length();
                                        }

                                        return new int[] {start, end};
                                    } else {
                                        poss++;
                                    }
                                }
                                if (token.getImage().length() == 2){
                                    poss--;
                                }
                            }

                        }
                    }
                    token = token.getPrevious();
                }

            } else{
                if ((token.getImage().length() == 1) && token.getImage().charAt(0) == '<'){
                    poss = 1;
                    TokenItem hToken;
                    while ( token != null){
                        if (token.getTokenID().getNumericID() == JspTagTokenContext.TAG_ID) {
                            if (token.getImage().trim().equals(tag)){
                                hToken = token;
                                while (token != null && token.getTokenID().getNumericID() != JspTagTokenContext.SYMBOL_ID) {
                                    token = token.getPrevious();
                                }
                                if (token != null) {
                                    if (token.getImage().length() == 2){
                                        if (poss == 0){
                                            start = token.getOffset();
                                            end = hToken.getOffset()+hToken.getImage().length()+1;
                                            token = token.getNext();

                                            while (token != null && (token.getTokenID().getNumericID() != JspTagTokenContext.SYMBOL_ID
                                                    || token.getImage().charAt(0)!='>')){
                                                token = token.getNext();
                                            }
                                            if (token != null)
                                                end = token.getOffset()+1;
                                            return new int[] {start, end};
                                        } else {
                                            poss++;
                                        }
                                    }
                                    if (token.getImage().length() == 1){
                                        // see issue #229149
                                        while (token != null) {
                                            if (token.getTokenID().getNumericID() == JspTagTokenContext.SYMBOL_ID) {
                                                if ("/>".equals(token.getImage())) {        //NOI18N
                                                    break;
                                                } else if (">".equals(token.getImage())) {  //NOI18N
                                                    poss--;
                                                    break;
                                                }
                                            }
                                            token = token.getNext();
                                        }
                                    }
                                }
                                token = hToken;
                            }
                        }
                        token = token.getNext();
                    }
                }
            }
        }
        return null;
    }

    /** Finds out whether the given tagTokenItem is a part of a singleton tag (e.g. <div style=""/>).
     * @tagTokenItem a token item whithin a tag
     * @return true is the token is a part of singleton tag
     */
    public boolean isSingletonTag(TokenItem tagTokenItem) {
        TokenItem ti = tagTokenItem;
        while(ti != null) {
            if(ti.getTokenID() == JspTagTokenContext.SYMBOL){
                if("/>".equals(ti.getImage())) { // NOI18N
                    //it is a singleton tag => do not match
                    return true;
                }
                if(">".equals(ti.getImage())) return false; // NOI18N
            }
            //break the loop on TEXT or on another open tag symbol
            //(just to prevent long loop in case the tag is not closed)
            if(ti.getTokenID() == JspTagTokenContext.TEXT) break;

            ti = ti.getNext();
        }
        return false;
    }

    /** Get the array of token IDs that should be skipped when
     * searching for matching bracket. It usually includes comments
     * and character and string constants. Returns empty array by default.
     */
    @Override
    protected TokenID[] getBracketSkipTokens() {
        return JSP_BRACKET_SKIP_TOKENS;
    }

    public static TokenSequence tokenSequence(TokenHierarchy hi, Language language, int offset) {
        TokenSequence ts = hi.tokenSequence(language);
        if(ts == null) {
            //Html language is not top level one
            ts = hi.tokenSequence();
	    if(ts == null) {
		return null; //seems to might happen during j2ee ergonomics lazy load
	    }
            ts.move(offset);
            if(!ts.moveNext() && !ts.movePrevious()) {
                return null; //no token found
            } else {
                ts = ts.embedded(language);
            }
        }
        return ts;
    }


    /** Finder for the matching bracket. It gets the original bracket char
     * and searches for the appropriate matching bracket character.
     */
    public class BracketFinder extends ExtSyntaxSupport.BracketFinder {

        BracketFinder(char c) {
            super(c);
        }

        /** Check whether the bracketChar really contains
         * the bracket character. If so assign the matchChar
         * and moveCount variables.
         */
        @Override
        protected boolean updateStatus() {
            if (super.updateStatus())
                return true;
            boolean valid = true;
            switch (bracketChar) {
                case '<':
                    matchChar = '>';
                    moveCount = +1;
                    break;
                case '>':
                    matchChar = '<';
                    moveCount = -1;
                    break;
                default:
                    valid = false;
            }
            return valid;
        }

        boolean isValid() {
            return (moveCount != 0);
        }

    }

    public void fileFolderCreated(FileEvent fe) {
    }

    public void fileDataCreated(FileEvent fe) {
    }

    public void fileChanged(FileEvent fe) {
    }

    public void fileDeleted(FileEvent fe) {
        //refresh fileobject
        initFileObject();
    }

    public void fileRenamed(FileRenameEvent fe) {
    }

    public void fileAttributeChanged(FileAttributeEvent fe) {
    }
}
