IDEA-19372: Refactoring: Extract .tag file for static piece of JSP
[idea/community.git] / xml / impl / src / com / intellij / xml / util / XmlUtil.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.xml.util;
17
18 import com.intellij.codeInsight.completion.CompletionUtil;
19 import com.intellij.codeInsight.daemon.Validator;
20 import com.intellij.javaee.ExternalResourceManager;
21 import com.intellij.javaee.ExternalResourceManagerEx;
22 import com.intellij.javaee.ExternalResourceManagerImpl;
23 import com.intellij.javaee.UriUtil;
24 import com.intellij.lang.ASTNode;
25 import com.intellij.lang.Language;
26 import com.intellij.lang.ParserDefinition;
27 import com.intellij.lang.html.HTMLLanguage;
28 import com.intellij.lang.xhtml.XHTMLLanguage;
29 import com.intellij.lexer.Lexer;
30 import com.intellij.openapi.application.ApplicationManager;
31 import com.intellij.openapi.diagnostic.Logger;
32 import com.intellij.openapi.extensions.Extensions;
33 import com.intellij.openapi.fileTypes.FileType;
34 import com.intellij.openapi.fileTypes.StdFileTypes;
35 import com.intellij.openapi.module.Module;
36 import com.intellij.openapi.project.Project;
37 import com.intellij.openapi.util.Key;
38 import com.intellij.openapi.util.Pair;
39 import com.intellij.openapi.util.text.StringUtil;
40 import com.intellij.openapi.vfs.CharsetToolkit;
41 import com.intellij.openapi.vfs.LocalFileSystem;
42 import com.intellij.openapi.vfs.VfsUtil;
43 import com.intellij.openapi.vfs.VirtualFile;
44 import com.intellij.patterns.StandardPatterns;
45 import com.intellij.patterns.StringPattern;
46 import com.intellij.patterns.XmlPatterns;
47 import com.intellij.psi.*;
48 import com.intellij.psi.XmlElementFactory;
49 import com.intellij.psi.filters.ElementFilter;
50 import com.intellij.psi.filters.XmlTagFilter;
51 import com.intellij.psi.filters.position.FilterPattern;
52 import com.intellij.psi.impl.source.html.HtmlDocumentImpl;
53 import com.intellij.psi.impl.source.xml.XmlEntityRefImpl;
54 import com.intellij.psi.scope.processor.FilterElementProcessor;
55 import com.intellij.psi.search.PsiElementProcessor;
56 import com.intellij.psi.tree.IElementType;
57 import com.intellij.psi.util.*;
58 import com.intellij.psi.xml.*;
59 import com.intellij.util.*;
60 import com.intellij.util.containers.ContainerUtil;
61 import com.intellij.util.xmlb.JDOMXIncluder;
62 import com.intellij.xml.*;
63 import com.intellij.xml.impl.schema.ComplexTypeDescriptor;
64 import com.intellij.xml.impl.schema.TypeDescriptor;
65 import com.intellij.xml.impl.schema.XmlElementDescriptorImpl;
66 import com.intellij.xml.impl.schema.XmlNSDescriptorImpl;
67 import com.intellij.xml.index.IndexedRelevantResource;
68 import com.intellij.xml.index.XmlNamespaceIndex;
69 import org.jetbrains.annotations.NonNls;
70 import org.jetbrains.annotations.NotNull;
71 import org.jetbrains.annotations.Nullable;
72
73 import java.io.File;
74 import java.net.URL;
75 import java.util.*;
76 import java.util.regex.Matcher;
77
78 /**
79  * @author Mike
80  */
81 public class XmlUtil {
82   private static final Logger LOG = Logger.getInstance("#com.intellij.xml.util.XmlUtil");
83
84   @NonNls public static final String TAGLIB_1_2_URI = "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd";
85
86   @NonNls public static final String XML_SCHEMA_URI = "http://www.w3.org/2001/XMLSchema";
87   @NonNls public static final String XML_SCHEMA_URI2 = "http://www.w3.org/1999/XMLSchema";
88   @NonNls public static final String XML_SCHEMA_URI3 = "http://www.w3.org/2000/10/XMLSchema";
89   public static final String[] SCHEMA_URIS = {XML_SCHEMA_URI, XML_SCHEMA_URI2, XML_SCHEMA_URI3};
90   @NonNls public static final String XML_SCHEMA_INSTANCE_URI = "http://www.w3.org/2001/XMLSchema-instance";
91
92   @NonNls public static final String XSLT_URI = "http://www.w3.org/1999/XSL/Transform";
93   @NonNls public static final String XINCLUDE_URI = "http://www.w3.org/2001/XInclude";
94
95   @NonNls public static final String ANT_URI = "http://ant.apache.org/schema.xsd";
96   @NonNls public static final String XHTML_URI = "http://www.w3.org/1999/xhtml";
97   @NonNls public static final String HTML_URI = "http://www.w3.org/1999/html";
98   @NonNls public static final String EMPTY_URI = "";
99   @NonNls public static final Key<String> TEST_PATH = Key.create("TEST PATH");
100   @NonNls public static final String JSP_URI = "http://java.sun.com/JSP/Page";
101   @NonNls public static final String ANY_URI = "http://www.intellij.net/ns/any";
102
103   @NonNls public static final String JSTL_CORE_URI = "http://java.sun.com/jsp/jstl/core";
104   @NonNls public static final String JSTL_CORE_URI2 = "http://java.sun.com/jstl/core";
105   @NonNls public static final String JSTL_CORE_URI3 = "http://java.sun.com/jstl/core_rt";
106   @NonNls public static final String[] JSTL_CORE_URIS = {JSTL_CORE_URI, JSTL_CORE_URI2, JSTL_CORE_URI3};
107
108   @NonNls public static final String JSF_HTML_URI = "http://java.sun.com/jsf/html";
109   @NonNls public static final String JSF_CORE_URI = "http://java.sun.com/jsf/core";
110
111   @NonNls private static final String JSTL_FORMAT_URI = "http://java.sun.com/jsp/jstl/fmt";
112   @NonNls private static final String JSTL_FORMAT_URI2 = "http://java.sun.com/jstl/fmt";
113   @NonNls private static final String JSTL_FORMAT_URI3 = "http://java.sun.com/jstl/fmt_rt";
114   @NonNls public static final String[] JSTL_FORMAT_URIS = {JSTL_FORMAT_URI, JSTL_FORMAT_URI2, JSTL_FORMAT_URI3};
115
116   @NonNls public static final String SPRING_URI = "http://www.springframework.org/tags";
117   @NonNls public static final String SPRING_FORMS_URI = "http://www.springframework.org/tags/form";
118   @NonNls public static final String STRUTS_BEAN_URI = "http://struts.apache.org/tags-bean";
119   @NonNls public static final String STRUTS_BEAN_URI2 = "http://jakarta.apache.org/struts/tags-bean";
120   @NonNls public static final String APACHE_I18N_URI = "http://jakarta.apache.org/taglibs/i18n-1.0";
121   @NonNls public static final String STRUTS_LOGIC_URI = "http://struts.apache.org/tags-logic";
122   @NonNls public static final String STRUTS_HTML_URI = "http://struts.apache.org/tags-html";
123   @NonNls public static final String STRUTS_HTML_URI2 = "http://jakarta.apache.org/struts/tags-html";
124
125   @NonNls public static final String APACHE_TRINIDAD_URI = "http://myfaces.apache.org/trinidad";
126   @NonNls public static final String APACHE_TRINIDAD_HTML_URI = "http://myfaces.apache.org/trinidad/html";
127
128
129   @NonNls public static final String[] HIBERNATE_URIS =
130     {"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd", "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"};
131
132   @NonNls public static final String XSD_SIMPLE_CONTENT_TAG = "simpleContent";
133   @NonNls public static final String NO_NAMESPACE_SCHEMA_LOCATION_ATT = "noNamespaceSchemaLocation";
134   @NonNls public static final String SCHEMA_LOCATION_ATT = "schemaLocation";
135   @NonNls public static final String[] WEB_XML_URIS =
136     {"http://java.sun.com/xml/ns/j2ee", "http://java.sun.com/xml/ns/javaee", "http://java.sun.com/dtd/web-app_2_3.dtd",
137       "http://java.sun.com/j2ee/dtds/web-app_2_2.dtd"};
138   @NonNls public static final String FACELETS_URI = "http://java.sun.com/jsf/facelets";
139   @NonNls public static final String JSTL_FUNCTIONS_URI = "http://java.sun.com/jsp/jstl/functions";
140   @NonNls public static final String JSTL_FUNCTIONS_URI2 = "http://java.sun.com/jstl/functions";
141   @NonNls public static final String JSTL_FN_FACELET_URI = "com.sun.facelets.tag.jstl.fn.JstlFnLibrary";
142   @NonNls public static final String JSTL_CORE_FACELET_URI = "com.sun.facelets.tag.jstl.core.JstlCoreLibrary";
143   @NonNls public static final String TARGET_NAMESPACE_ATTR_NAME = "targetNamespace";
144   @NonNls public static final String XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
145   public static final List<String> ourSchemaUrisList = Arrays.asList(SCHEMA_URIS);
146   private static final ThreadLocal<String> XML_FILE_IN_PROGRESS = new ThreadLocal<String>();
147   public static final Key<Boolean> ANT_FILE_SIGN = new Key<Boolean>("FORCED ANT FILE");
148   @NonNls public static final String TAG_DIR_NS_PREFIX = "urn:jsptagdir:";
149   @NonNls public static final String VALUE_ATTR_NAME = "value";
150   @NonNls public static final String ENUMERATION_TAG_NAME = "enumeration";
151   public static final String HTML4_LOOSE_URI = "http://www.w3.org/TR/html4/loose.dtd";
152
153
154   private XmlUtil() {
155   }
156
157   @Nullable
158   public static String getSchemaLocation(XmlTag tag, String namespace) {
159     final String uri = ExternalResourceManagerEx.getInstanceEx().getResourceLocation(namespace, tag.getProject());
160     if (uri != null && !uri.equals(namespace)) return uri;
161
162     while (true) {
163       if ("".equals(namespace)) {
164         final String attributeValue = tag.getAttributeValue("noNamespaceSchemaLocation", XML_SCHEMA_INSTANCE_URI);
165         if (attributeValue != null) return attributeValue;
166       }
167       else {
168         String schemaLocation = tag.getAttributeValue("schemaLocation", XML_SCHEMA_INSTANCE_URI);
169         if (schemaLocation != null) {
170           int start = schemaLocation.indexOf(namespace);
171           if (start >= 0) {
172             start += namespace.length();
173             final StringTokenizer tokenizer = new StringTokenizer(schemaLocation.substring(start + 1));
174             if (tokenizer.hasMoreTokens()) {
175               return tokenizer.nextToken();
176             }
177             else {
178               return null;
179             }
180           }
181         }
182       }
183       if (tag.getParent() instanceof XmlTag) {
184         tag = (XmlTag)tag.getParent();
185       }
186       else {
187         break;
188       }
189     }
190     return null;
191   }
192
193   @SuppressWarnings({"ConstantConditions"})
194   public static XmlTag getRootTag(XmlFile file) {
195     if (file == null) return null;
196     final XmlDocument document = file.getDocument();
197     if (document == null) return null;
198     return document.getRootTag();
199   }
200
201   @Nullable
202   public static String findNamespacePrefixByURI(XmlFile file, @NonNls String uri) {
203     final XmlTag tag = getRootTag(file);
204     if (tag == null) return null;
205
206     for (XmlAttribute attribute : tag.getAttributes()) {
207       if (attribute.getName().startsWith("xmlns:") && attribute.getValue().equals(uri)) {
208         return attribute.getName().substring("xmlns:".length());
209       }
210       if ("xmlns".equals(attribute.getName()) && attribute.getValue().equals(uri)) return "";
211     }
212
213     return null;
214   }
215
216   public static String[] findNamespacesByURI(XmlFile file, String uri) {
217     if (file == null) return ArrayUtil.EMPTY_STRING_ARRAY;
218     final XmlDocument document = file.getDocument();
219     if (document == null) return ArrayUtil.EMPTY_STRING_ARRAY;
220     final XmlTag tag = document.getRootTag();
221     if (tag == null) return ArrayUtil.EMPTY_STRING_ARRAY;
222     XmlAttribute[] attributes = tag.getAttributes();
223
224
225     List<String> result = new ArrayList<String>();
226
227     for (XmlAttribute attribute : attributes) {
228       if (attribute.getName().startsWith("xmlns:") && attribute.getValue().equals(uri)) {
229         result.add(attribute.getName().substring("xmlns:".length()));
230       }
231       if ("xmlns".equals(attribute.getName()) && attribute.getValue().equals(uri)) result.add("");
232     }
233
234     return ArrayUtil.toStringArray(result);
235   }
236
237   @Nullable
238   public static String getXsiNamespace(XmlFile file) {
239     return findNamespacePrefixByURI(file, XML_SCHEMA_INSTANCE_URI);
240   }
241
242   @Nullable
243   public static XmlFile findNamespace(PsiFile base, @NotNull String nsLocation) {
244     final String location = ExternalResourceManager.getInstance().getResourceLocation(nsLocation, base.getProject());
245     if (!location.equals(nsLocation)) { // is mapped
246       return findXmlFile(base, location);
247     }
248     final XmlFile xmlFile = XmlSchemaProvider.findSchema(location, base);
249     return xmlFile == null ? findXmlFile(base, location) : xmlFile;
250   }
251
252   @Nullable
253   public static XmlFile findNamespaceByLocation(PsiFile base, @NotNull String nsLocation) {
254     final String location = ExternalResourceManager.getInstance().getResourceLocation(nsLocation, base.getProject());
255     return findXmlFile(base, location);
256   }
257
258   public static Collection<XmlFile> findNSFilesByURI(String namespace, final Project project, Module module) {
259     final List<IndexedRelevantResource<String,String>> resources = XmlNamespaceIndex.getResourcesByNamespace(namespace, project, module);
260     final PsiManager psiManager = PsiManager.getInstance(project);
261     return ContainerUtil.mapNotNull(resources, new NullableFunction<IndexedRelevantResource<String, String>, XmlFile>() {
262       public XmlFile fun(IndexedRelevantResource<String, String> stringStringIndexedRelevantResource) {
263         PsiFile file = psiManager.findFile(stringStringIndexedRelevantResource.getFile());
264         return file instanceof XmlFile ? (XmlFile)file : null;
265       }
266     });
267   }
268
269   @Nullable
270   public static XmlFile findXmlFile(PsiFile base, @NotNull String uri) {
271     PsiFile result = null;
272
273     if (ApplicationManager.getApplication().isUnitTestMode()) {
274       String data = base.getOriginalFile().getUserData(TEST_PATH);
275
276       if (data != null) {
277         String filePath = data + "/" + uri;
278         final VirtualFile path = LocalFileSystem.getInstance().findFileByPath(filePath.replace(File.separatorChar, '/'));
279         if (path != null) {
280           result = base.getManager().findFile(path);
281         }
282       }
283     }
284     if (result == null) {
285       result = findRelativeFile(uri, base);
286     }
287
288     if (result instanceof XmlFile) {
289       return (XmlFile)result;
290     }
291
292     return null;
293   }
294
295   @Nullable
296   public static XmlToken getTokenOfType(PsiElement element, IElementType type) {
297     if (element == null) {
298       return null;
299     }
300
301     PsiElement[] children = element.getChildren();
302
303     for (PsiElement child : children) {
304       if (child instanceof XmlToken) {
305         XmlToken token = (XmlToken)child;
306
307         if (token.getTokenType() == type) {
308           return token;
309         }
310       }
311     }
312
313     return null;
314   }
315
316   public static boolean processXmlElements(XmlElement element, PsiElementProcessor processor, boolean deepFlag) {
317     return processXmlElements(element, processor, deepFlag, false);
318   }
319
320   public static boolean processXmlElements(XmlElement element, PsiElementProcessor processor, boolean deepFlag, boolean wideFlag) {
321     if (element == null) return true;
322     PsiFile baseFile = element.isValid() ? element.getContainingFile() : null;
323     return processXmlElements(element, processor, deepFlag, wideFlag, baseFile);
324   }
325
326   public static boolean processXmlElements(final XmlElement element,
327                                            final PsiElementProcessor processor,
328                                            final boolean deepFlag,
329                                            final boolean wideFlag,
330                                            final PsiFile baseFile) {
331     return new XmlElementProcessor(processor, baseFile).processXmlElements(element, deepFlag, wideFlag);
332   }
333
334   public static boolean processXmlElementChildren(final XmlElement element, final PsiElementProcessor processor, final boolean deepFlag) {
335     final XmlElementProcessor p = new XmlElementProcessor(processor, element.getContainingFile());
336
337     final boolean wideFlag = false;
338     for (PsiElement child = element.getFirstChild(); child != null; child = child.getNextSibling()) {
339       if (!p.processElement(child, deepFlag, wideFlag) && !wideFlag) return false;
340     }
341
342     return true;
343   }
344
345   public static ParserDefinition.SpaceRequirements canStickTokensTogetherByLexerInXml(final ASTNode left,
346                                                                                       final ASTNode right,
347                                                                                       final Lexer lexer,
348                                                                                       int state) {
349     if (left.getElementType() == XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN ||
350         right.getElementType() == XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN) {
351       return ParserDefinition.SpaceRequirements.MUST_NOT;
352     }
353     if (left.getElementType() == XmlTokenType.XML_ATTRIBUTE_VALUE_END_DELIMITER && right.getElementType() == XmlTokenType.XML_NAME) {
354       return ParserDefinition.SpaceRequirements.MUST;
355     }
356     if (left.getElementType() == XmlTokenType.XML_NAME && right.getElementType() == XmlTokenType.XML_NAME) {
357       return ParserDefinition.SpaceRequirements.MUST;
358     }
359     return ParserDefinition.SpaceRequirements.MAY;
360   }
361
362   public static boolean tagFromTemplateFramework(@NotNull final XmlTag tag) {
363     final String ns = tag.getNamespace();
364     return nsFromTemplateFramework(ns);
365   }
366
367   public static boolean nsFromTemplateFramework(final String ns) {
368     return XSLT_URI.equals(ns) || XINCLUDE_URI.equals(ns);
369   }
370
371
372   public static char getCharFromEntityRef(@NonNls String text) {
373     //LOG.assertTrue(text.startsWith("&#") && text.endsWith(";"));
374     if (text.charAt(1) != '#') {
375       text = text.substring(1, text.length() - 1);
376       return XmlTagUtil.getCharacterByEntityName(text);
377     }
378     text = text.substring(2, text.length() - 1);
379     try {
380       int code;
381       if (StringUtil.startsWithChar(text, 'x')) {
382         text = text.substring(1);
383         code = Integer.parseInt(text, 16);
384       }
385       else {
386         code = Integer.parseInt(text);
387       }
388       return (char)code;
389     }
390     catch (NumberFormatException e) {
391       return 0;
392     }
393   }
394
395   public static boolean attributeFromTemplateFramework(@NonNls final String name, final XmlTag tag) {
396     return "jsfc".equals(name) && tag.getNSDescriptor(JSF_HTML_URI, true) != null;
397   }
398
399   @Nullable
400   public static String getTargetSchemaNsFromTag(@Nullable final XmlTag xmlTag) {
401     if (xmlTag == null) return null;
402     String targetNamespace = xmlTag.getAttributeValue(TARGET_NAMESPACE_ATTR_NAME, XML_SCHEMA_URI);
403     if (targetNamespace == null) targetNamespace = xmlTag.getAttributeValue(TARGET_NAMESPACE_ATTR_NAME, XML_SCHEMA_URI2);
404     if (targetNamespace == null) targetNamespace = xmlTag.getAttributeValue(TARGET_NAMESPACE_ATTR_NAME, XML_SCHEMA_URI3);
405     return targetNamespace;
406   }
407
408   @Nullable
409   public static XmlTag getSchemaSimpleContent(@NotNull XmlTag tag) {
410     XmlElementDescriptor descriptor = tag.getDescriptor();
411
412     if (descriptor instanceof XmlElementDescriptorImpl) {
413       final TypeDescriptor type = ((XmlElementDescriptorImpl)descriptor).getType(tag);
414
415       if (type instanceof ComplexTypeDescriptor) {
416         final XmlTag[] simpleContent = new XmlTag[1];
417
418         processXmlElements(((ComplexTypeDescriptor)type).getDeclaration(), new PsiElementProcessor() {
419           public boolean execute(final PsiElement element) {
420             if (element instanceof XmlTag) {
421               final XmlTag tag = (XmlTag)element;
422               @NonNls final String s = ((XmlTag)element).getLocalName();
423
424               if ((s.equals(XSD_SIMPLE_CONTENT_TAG) ||
425                    s.equals("restriction") && "string".equals(findLocalNameByQualifiedName(tag.getAttributeValue("base")))) &&
426                   tag.getNamespace().equals(XML_SCHEMA_URI)) {
427                 simpleContent[0] = tag;
428                 return false;
429               }
430             }
431
432             return true;
433           }
434         }, true);
435
436         return simpleContent[0];
437       }
438     }
439     return null;
440   }
441
442   public static <T extends PsiElement> void doDuplicationCheckForElements(final T[] elements,
443                                                                           final Map<String, T> presentNames,
444                                                                           DuplicationInfoProvider<T> provider,
445                                                                           final Validator.ValidationHost host) {
446     for (T t : elements) {
447       final String name = provider.getName(t);
448       if (name == null) continue;
449
450       final String nameKey = provider.getNameKey(t, name);
451
452       if (presentNames.containsKey(nameKey)) {
453         final T psiElement = presentNames.get(nameKey);
454         final String message = XmlBundle.message("duplicate.declaration", nameKey);
455
456         if (psiElement != null) {
457           presentNames.put(nameKey, null);
458
459           host.addMessage(provider.getNodeForMessage(psiElement), message, Validator.ValidationHost.ERROR);
460         }
461
462         host.addMessage(provider.getNodeForMessage(t), message, Validator.ValidationHost.ERROR);
463       }
464       else {
465         presentNames.put(nameKey, t);
466       }
467     }
468   }
469
470   public static String getEntityValue(final XmlEntityRef entityRef) {
471     final XmlEntityDecl decl = entityRef.resolve(entityRef.getContainingFile());
472     if (decl != null) {
473       final XmlAttributeValue valueElement = decl.getValueElement();
474       if (valueElement != null) {
475         final String value = valueElement.getValue();
476         if (value != null) {
477           return value;
478         }
479       }
480     }
481     return entityRef.getText();
482   }
483
484   public static boolean isAntFile(final PsiFile file) {
485     if (file instanceof XmlFile) {
486       final XmlFile xmlFile = (XmlFile)file;
487       final XmlDocument document = xmlFile.getDocument();
488       if (document != null) {
489         final XmlTag tag = document.getRootTag();
490         if (tag != null && "project".equals(tag.getName()) && tag.getContext() instanceof XmlDocument) {
491           if (tag.getAttributeValue("default") != null) {
492             return true;
493           }
494           VirtualFile vFile = xmlFile.getOriginalFile().getVirtualFile();
495           if (vFile != null && vFile.getUserData(ANT_FILE_SIGN) != null) {
496             return true;
497           }
498         }
499       }
500     }
501     return false;
502   }
503
504   @Nullable
505   public static PsiFile findRelativeFile(String uri, PsiElement base) {
506     if (base instanceof PsiFile) {
507       PsiFile baseFile = (PsiFile)base;
508       VirtualFile file = UriUtil.findRelative(uri, baseFile.getOriginalFile());
509       if (file == null) return null;
510       return base.getManager().findFile(file);
511     }
512     else if (base instanceof PsiDirectory) {
513       PsiDirectory baseDir = (PsiDirectory)base;
514       VirtualFile file = UriUtil.findRelative(uri, baseDir);
515       if (file == null) return null;
516       return base.getManager().findFile(file);
517     }
518
519     return null;
520   }
521
522   @Nullable
523   public static String getCommentText(XmlComment comment) {
524     final PsiElement firstChild = comment.getFirstChild();
525     if (firstChild != null) {
526       final PsiElement nextSibling = firstChild.getNextSibling();
527       if (nextSibling instanceof XmlToken) {
528         final XmlToken token = (XmlToken)nextSibling;
529         if (token.getTokenType() == XmlTokenType.XML_COMMENT_CHARACTERS) {
530           return token.getText();
531         }
532       }
533     }
534     return null;
535   }
536
537   @Nullable
538   public static PsiElement findNamespaceDeclaration(XmlElement xmlElement, String nsName) {
539     while (! (xmlElement instanceof XmlTag) && xmlElement != null) {
540       final PsiElement parent = xmlElement.getParent();
541       if (!(parent instanceof XmlElement)) return null;
542       xmlElement = (XmlElement)parent;
543     }
544     if (xmlElement != null) {
545       XmlTag tag = (XmlTag)xmlElement;
546       while (tag != null) {
547         for (XmlAttribute attribute : tag.getAttributes()) {
548           if (attribute.isNamespaceDeclaration() && attribute.getLocalName().equals(nsName)) {
549             return attribute;
550           }
551         }
552         tag = tag.getParentTag();
553       }
554     }
555     return null;
556   }
557
558   private static class XmlElementProcessor {
559     private final PsiElementProcessor processor;
560     private final PsiFile targetFile;
561     private static final Key<CachedValue<PsiElement[]>> KEY_RESOLVED_XINCLUDE = Key.create("RESOLVED_XINCLUDE");
562
563     XmlElementProcessor(PsiElementProcessor _processor, PsiFile _targetFile) {
564       processor = _processor;
565       targetFile = _targetFile;
566     }
567
568     private boolean processXmlElements(PsiElement element, boolean deepFlag, boolean wideFlag) {
569       if (deepFlag) if (!processor.execute(element)) return false;
570
571       PsiElement startFrom = element.getFirstChild();
572
573       if (element instanceof XmlEntityRef) {
574         XmlEntityRef ref = (XmlEntityRef)element;
575
576         //if ("Number.datatype".equals(ref.getText().substring(1, ref.getTextLength() - 1))) {
577         //  int a = 1;
578         //}
579         PsiElement newElement = parseEntityRef(targetFile, ref, true);
580         //if (newElement == null) {
581         //  System.out.println("No image for :" + ref.getText());
582         //  return true;
583         //}
584
585         while (newElement != null) {
586           if (!processElement(newElement, deepFlag, wideFlag)) return false;
587           newElement = newElement.getNextSibling();
588         }
589
590         return true;
591       }
592       else if (element instanceof XmlConditionalSection) {
593         XmlConditionalSection xmlConditionalSection = (XmlConditionalSection)element;
594         if (!xmlConditionalSection.isIncluded(targetFile)) return true;
595         startFrom = xmlConditionalSection.getBodyStart();
596       }
597       else if (XmlIncludeHandler.isXInclude(element)) {
598         XmlTag tag = (XmlTag)element;
599         if (!processXInclude(deepFlag, wideFlag, tag)) return false;
600       }
601
602       for (PsiElement child = startFrom; child != null; child = child.getNextSibling()) {
603         if (!processElement(child, deepFlag, wideFlag) && !wideFlag) return false;
604       }
605
606       return true;
607     }
608
609     private boolean processXInclude(final boolean deepFlag, final boolean wideFlag, final XmlTag xincludeTag) {
610
611       final PsiElement[] inclusion = CachedValuesManager.getManager(xincludeTag.getProject()).getCachedValue(xincludeTag, KEY_RESOLVED_XINCLUDE, new CachedValueProvider<PsiElement[]>() {
612           public Result<PsiElement[]> compute() {
613             final Result<PsiElement[]> result = computeInclusion(xincludeTag);
614             result.setLockValue(true);
615             return result;
616           }
617         }, false);
618
619       if (inclusion != null) {
620         for (PsiElement psiElement : inclusion) {
621           if (!processElement(psiElement, deepFlag, wideFlag)) return false;
622         }
623       }
624
625       return true;
626     }
627
628     private static CachedValueProvider.Result<PsiElement[]> computeInclusion(final XmlTag xincludeTag) {
629       List<Object> deps = new ArrayList<Object>();
630       deps.add(xincludeTag.getContainingFile());
631
632       final XmlFile xmlFile = XmlIncludeHandler.resolveXIncludeFile(xincludeTag);
633
634       PsiElement[] result = null;
635       if (xmlFile != null) {
636         deps.add(xmlFile);
637         final XmlDocument document = xmlFile.getDocument();
638         if (document != null) {
639           XmlTag rootTag = document.getRootTag();
640           if (rootTag != null) {
641             final XmlTag[] includeTag = extractXpointer(rootTag, xincludeTag);
642             result = ContainerUtil.map(includeTag, new Function<XmlTag, PsiElement>() {
643               public PsiElement fun(final XmlTag xmlTag) {
644                 final PsiElement psiElement = PsiUtilBase.copyElementPreservingOriginalLinks(xmlTag, XmlElement.ORIGINAL_ELEMENT);
645                 psiElement.putUserData(XmlElement.INCLUDING_ELEMENT, xincludeTag.getParentTag());
646                 psiElement.putUserData(XmlElement.ORIGINAL_ELEMENT, xmlTag);
647                 return psiElement;
648               }
649             }, new PsiElement[includeTag.length]);
650           }
651         }
652       }
653
654       return new CachedValueProvider.Result<PsiElement[]>(result, deps.toArray());
655     }
656
657     private static XmlTag[] extractXpointer(XmlTag rootTag, final XmlTag xincludeTag) {
658       final String xpointer = xincludeTag.getAttributeValue("xpointer", XINCLUDE_URI);
659
660       if (xpointer != null) {
661         Matcher matcher = JDOMXIncluder.XPOINTER_PATTERN.matcher(xpointer);
662         if (matcher.matches()) {
663           String pointer = matcher.group(1);
664           matcher = JDOMXIncluder.CHILDREN_PATTERN.matcher(pointer);
665
666           if (matcher.matches()) {
667             final String rootTagName = matcher.group(1);
668
669             if (rootTagName.equals(rootTag.getName())) return rootTag.getSubTags();
670           }
671         }
672       }
673
674       return new XmlTag[]{rootTag};
675     }
676
677     private boolean processElement(PsiElement child, boolean deepFlag, boolean wideFlag) {
678       if (deepFlag) {
679         if (!processXmlElements(child, true, wideFlag)) {
680           return false;
681         }
682       }
683       else {
684         if (child instanceof XmlEntityRef) {
685           if (!processXmlElements(child, false, wideFlag)) return false;
686         }
687         else if (child instanceof XmlConditionalSection) {
688           if (!processXmlElements(child, false, wideFlag)) return false;
689         }
690         else if (XmlIncludeHandler.isXInclude(child)) {
691           if (!processXmlElements(child, false, wideFlag)) return false;
692         }
693         else if (!processor.execute(child)) return false;
694       }
695       if (targetFile != null && child instanceof XmlEntityDecl) {
696         XmlEntityDecl xmlEntityDecl = (XmlEntityDecl)child;
697         XmlEntityRefImpl.cacheParticularEntity(targetFile, xmlEntityDecl);
698       }
699       return true;
700     }
701
702   }
703
704   private static PsiElement parseEntityRef(PsiFile targetFile, XmlEntityRef ref, boolean cacheValue) {
705     int type = getContextType(ref);
706
707     {
708       final XmlEntityDecl entityDecl = ref.resolve(targetFile);
709       if (entityDecl != null) return parseEntityDecl(entityDecl, targetFile, type, cacheValue, ref);
710     }
711
712     PsiElement e = ref;
713     while (e != null) {
714       if (e.getUserData(XmlElement.INCLUDING_ELEMENT) != null) {
715         e = e.getUserData(XmlElement.INCLUDING_ELEMENT);
716         final PsiFile f = e.getContainingFile();
717         if (f != null) {
718           final XmlEntityDecl entityDecl = ref.resolve(targetFile);
719           if (entityDecl != null) return parseEntityDecl(entityDecl, targetFile, type, cacheValue, ref);
720         }
721
722         continue;
723       }
724       if (e instanceof PsiFile) {
725         PsiFile refFile = (PsiFile)e;
726         final XmlEntityDecl entityDecl = ref.resolve(refFile);
727         if (entityDecl != null) return parseEntityDecl(entityDecl, targetFile, type, cacheValue, ref);
728         break;
729       }
730
731       e = e.getParent();
732     }
733
734     final PsiElement element = ref.getUserData(XmlElement.DEPENDING_ELEMENT);
735     if (element instanceof XmlFile) {
736       final XmlEntityDecl entityDecl = ref.resolve((PsiFile)element);
737       if (entityDecl != null) return parseEntityDecl(entityDecl, targetFile, type, cacheValue, ref);
738     }
739
740     return null;
741   }
742
743   private static int getContextType(XmlEntityRef ref) {
744     int type = XmlEntityDecl.CONTEXT_GENERIC_XML;
745     PsiElement temp = ref;
746     while (temp != null) {
747       if (temp instanceof XmlAttributeDecl) {
748         type = XmlEntityDecl.CONTEXT_ATTRIBUTE_SPEC;
749       }
750       else if (temp instanceof XmlElementDecl) {
751         type = XmlEntityDecl.CONTEXT_ELEMENT_CONTENT_SPEC;
752       }
753       else if (temp instanceof XmlAttlistDecl) {
754         type = XmlEntityDecl.CONTEXT_ATTLIST_SPEC;
755       }
756       else if (temp instanceof XmlEntityDecl) {
757         type = XmlEntityDecl.CONTEXT_ENTITY_DECL_CONTENT;
758       }
759       else if (temp instanceof XmlEnumeratedType) {
760         type = XmlEntityDecl.CONTEXT_ENUMERATED_TYPE;
761       }
762       else if (temp instanceof XmlAttributeValue) {
763         type = XmlEntityDecl.CONTEXT_ATTR_VALUE;
764       }
765       else {
766         temp = temp.getContext();
767         continue;
768       }
769       break;
770     }
771     return type;
772   }
773
774   private static final Key<CachedValue<PsiElement>> PARSED_DECL_KEY = Key.create("PARSED_DECL_KEY");
775
776   private static PsiElement parseEntityDecl(final XmlEntityDecl entityDecl,
777                                             final PsiFile targetFile,
778                                             final int type,
779                                             boolean cacheValue,
780                                             final XmlEntityRef entityRef) {
781     if (!cacheValue) return entityDecl.parse(targetFile, type, entityRef);
782
783     synchronized (PsiLock.LOCK) { // we depend on targetFile and entityRef
784       CachedValue<PsiElement> value = entityRef.getUserData(PARSED_DECL_KEY);
785       //    return entityDecl.parse(targetFile, type);
786
787       if (value == null) {
788         value = CachedValuesManager.getManager(entityDecl.getProject()).createCachedValue(new CachedValueProvider<PsiElement>() {
789           public Result<PsiElement> compute() {
790             final PsiElement res = entityDecl.parse(targetFile, type, entityRef);
791             if (res == null) return new Result<PsiElement>(res, targetFile);
792             if (!entityDecl.isInternalReference()) XmlEntityRefImpl.copyEntityCaches(res.getContainingFile(), targetFile);
793             return new Result<PsiElement>(res, res.getUserData(XmlElement.DEPENDING_ELEMENT), entityDecl, targetFile, entityRef);
794           }
795         }, false);
796         entityRef.putUserData(PARSED_DECL_KEY, value);
797       }
798
799       return value.getValue();
800     }
801   }
802
803   /**
804    * add child to the parent according to DTD/Schema element ordering
805    *
806    * @return newly added child
807    */
808   public static XmlTag addChildTag(XmlTag parent, XmlTag child) throws IncorrectOperationException {
809     return addChildTag(parent, child, -1);
810   }
811
812   public static XmlTag addChildTag(XmlTag parent, XmlTag child, int index) throws IncorrectOperationException {
813
814     // bug in PSI: cannot add child to <tag/>
815     if (parent.getSubTags().length == 0 && parent.getText().endsWith("/>")) {
816       final XmlElementFactory factory = XmlElementFactory.getInstance(parent.getProject());
817       final String name = parent.getName();
818       final String text = parent.getText();
819       final XmlTag tag = factory.createTagFromText(text.substring(0, text.length() - 2) + "></" + name + ">");
820       parent = (XmlTag)parent.replace(tag);
821     }
822
823     final XmlElementDescriptor parentDescriptor = parent.getDescriptor();
824     final XmlTag[] subTags = parent.getSubTags();
825     if (parentDescriptor == null || subTags.length == 0) return (XmlTag)parent.add(child);
826     int subTagNum = -1;
827
828     for (XmlElementDescriptor childElementDescriptor : parentDescriptor.getElementsDescriptors(parent)) {
829       final String childElementName = childElementDescriptor.getName();
830       int prevSubTagNum = subTagNum;
831       while (subTagNum < subTags.length - 1 && subTags[subTagNum + 1].getName().equals(childElementName)) {
832         subTagNum++;
833       }
834       if (childElementName.equals(child.getLocalName())) {
835         // insert child just after anchor
836         // insert into the position specified by index
837         subTagNum = index == -1 || index > subTagNum - prevSubTagNum ? subTagNum : prevSubTagNum + index;
838         return (XmlTag)(subTagNum == -1 ? parent.addBefore(child, subTags[0]) : parent.addAfter(child, subTags[subTagNum]));
839       }
840     }
841     return (XmlTag)parent.add(child);
842   }
843
844   public static String getAttributeValue(XmlTag tag, String name) {
845     for (XmlAttribute attribute : tag.getAttributes()) {
846       if (name.equals(attribute.getName())) return attribute.getValue();
847     }
848     return null;
849   }
850
851   public static XmlTag findOnAnyLevel(XmlTag root, String[] chain) {
852     XmlTag curTag = root;
853     for (String s : chain) {
854       curTag = curTag.findFirstSubTag(s);
855       if (curTag == null) return null;
856     }
857
858     return curTag;
859   }
860
861   public static XmlTag findSubTag(XmlTag rootTag, String path) {
862     String[] pathElements = path.split("/");
863
864     XmlTag curTag = rootTag;
865     for (String curTagName : pathElements) {
866       curTag = curTag.findFirstSubTag(curTagName);
867       if (curTag == null) break;
868     }
869     return curTag;
870   }
871
872   @Nullable
873   public static XmlTag findSubTagWithValue(XmlTag rootTag, String tagName, String tagValue) {
874     if (rootTag == null) return null;
875     final XmlTag[] subTags = rootTag.findSubTags(tagName);
876     for (XmlTag subTag : subTags) {
877       if (subTag.getValue().getTrimmedText().equals(tagValue)) {
878         return subTag;
879       }
880     }
881     return null;
882   }
883
884   // Read the function name and parameter names to find out what this function does... :-)
885   @Nullable
886   public static XmlTag find(String subTag, String withValue, String forTag, XmlTag insideRoot) {
887     final XmlTag[] forTags = insideRoot.findSubTags(forTag);
888
889     for (XmlTag tag : forTags) {
890       final XmlTag[] allTags = tag.findSubTags(subTag);
891
892       for (XmlTag curTag : allTags) {
893         if (curTag.getName().equals(subTag) && curTag.getValue().getTrimmedText().equalsIgnoreCase(withValue)) {
894           return tag;
895         }
896       }
897     }
898
899     return null;
900   }
901
902   @Nullable
903   @NonNls
904   public static String[][] getDefaultNamespaces(final XmlDocument document) {
905     final XmlFile file = getContainingFile(document);
906
907     final XmlTag tag = document.getRootTag();
908     if (tag == null) return null;
909
910     if (file != null) {
911       @NotNull final XmlFileNSInfoProvider[] nsProviders = Extensions.getExtensions(XmlFileNSInfoProvider.EP_NAME);
912
913       NextProvider:
914       for (XmlFileNSInfoProvider nsProvider : nsProviders) {
915         final String[][] pairs = nsProvider.getDefaultNamespaces(file);
916         if (pairs != null && pairs.length > 0) {
917
918           for (final String[] nsMapping : pairs) {
919             if (nsMapping == null || nsMapping.length != 2 || nsMapping[0] == null || nsMapping[1] == null) {
920               LOG.debug("NSInfoProvider " + nsProvider + " gave wrong info about " + file.getVirtualFile());
921               continue NextProvider;
922             }
923           }
924           return pairs;
925         }
926       }
927     }
928
929     String namespace = getDtdUri(document);
930
931     if (namespace != null) {
932       boolean overrideNamespaceFromDocType = false;
933
934       if (file != null) {
935         final FileType fileType = file.getFileType();
936         overrideNamespaceFromDocType =
937           fileType == StdFileTypes.HTML || fileType == StdFileTypes.XHTML || fileType == StdFileTypes.JSPX || fileType == StdFileTypes.JSP;
938       }
939
940       if (!overrideNamespaceFromDocType) return new String[][]{new String[]{"", namespace}};
941     }
942
943     if ("taglib".equals(tag.getName())) {
944       return new String[][]{new String[]{"", TAGLIB_1_2_URI}};
945     }
946
947     if (file != null) {
948
949       final Language language = file.getLanguage();
950       if (language == HTMLLanguage.INSTANCE || language == XHTMLLanguage.INSTANCE) {
951         return new String[][]{new String[]{"", XHTML_URI}};
952       }
953     }
954
955     return null;
956   }
957
958   private static final String HTML5_SCHEMA_LOCATION;
959   
960   static {
961     URL schemaLocationURL = XmlUtil.class.getResource(ExternalResourceManagerImpl.STANDARD_SCHEMAS + "html5/xhtml5.xsd");
962     VirtualFile relativeFile = schemaLocationURL != null ? VfsUtil.findFileByURL(schemaLocationURL):null;
963     HTML5_SCHEMA_LOCATION = relativeFile != null ? relativeFile.getPath():"";
964   }
965   
966   @Nullable
967   public static String getDtdUri(XmlDocument document) {
968     XmlProlog prolog = document.getProlog();
969     if (prolog != null) {
970       return getDtdUri( prolog.getDoctype() );
971     }
972     return null;
973   }
974   
975   @Nullable
976   public static String getDtdUri(XmlDoctype doctype) {    
977     if (doctype != null) {
978       String docType = doctype.getDtdUri();
979       if (docType == null && 
980           PsiTreeUtil.getParentOfType(doctype, XmlDocument.class) instanceof HtmlDocumentImpl) {
981         
982         final String publicId = doctype.getPublicId();
983         if (publicId != null && publicId.indexOf("-//W3C//DTD HTML") != -1) {
984           return HTML4_LOOSE_URI;
985         }
986         if (publicId == null) docType = HTML5_SCHEMA_LOCATION;
987       }
988       return docType;
989     }
990     return null;
991   }
992
993   private static void computeTag(XmlTag tag,
994                                  final Map<String, List<String>> tagsMap,
995                                  final Map<String, List<MyAttributeInfo>> attributesMap) {
996     if (tag == null) {
997       return;
998     }
999     final String tagName = tag.getName();
1000
1001     List<MyAttributeInfo> list = attributesMap.get(tagName);
1002     if (list == null) {
1003       list = new ArrayList<MyAttributeInfo>();
1004       final XmlAttribute[] attributes = tag.getAttributes();
1005       for (final XmlAttribute attribute : attributes) {
1006         list.add(new MyAttributeInfo(attribute.getName()));
1007       }
1008     }
1009     else {
1010       final XmlAttribute[] attributes = tag.getAttributes();
1011       ContainerUtil.sort(list);
1012       Arrays.sort(attributes, new Comparator<XmlAttribute>() {
1013         public int compare(XmlAttribute attr1, XmlAttribute attr2) {
1014           return attr1.getName().compareTo(attr2.getName());
1015         }
1016       });
1017
1018       final Iterator<MyAttributeInfo> iter = list.iterator();
1019       list = new ArrayList<MyAttributeInfo>();
1020       int index = 0;
1021       while (iter.hasNext()) {
1022         final MyAttributeInfo info = iter.next();
1023         boolean requiredFlag = false;
1024         while (attributes.length > index) {
1025           if (info.compareTo(attributes[index]) != 0) {
1026             if (info.compareTo(attributes[index]) < 0) {
1027               break;
1028             }
1029             if (attributes[index].getValue() != null) list.add(new MyAttributeInfo(attributes[index].getName(), false));
1030             index++;
1031           }
1032           else {
1033             requiredFlag = true;
1034             index++;
1035             break;
1036           }
1037         }
1038         info.myRequired &= requiredFlag;
1039         list.add(info);
1040       }
1041       while (attributes.length > index) {
1042         if (attributes[index].getValue() != null) {
1043           list.add(new MyAttributeInfo(attributes[index++].getName(), false));
1044         }
1045         else {
1046           index++;
1047         }
1048       }
1049     }
1050     attributesMap.put(tagName, list);
1051     final List<String> tags = tagsMap.get(tagName) != null ? tagsMap.get(tagName) : new ArrayList<String>();
1052     tagsMap.put(tagName, tags);
1053     tag.processElements(new FilterElementProcessor(XmlTagFilter.INSTANCE) {
1054       public void add(PsiElement element) {
1055         XmlTag tag = (XmlTag)element;
1056         if (!tags.contains(tag.getName())) {
1057           tags.add(tag.getName());
1058         }
1059         computeTag(tag, tagsMap, attributesMap);
1060       }
1061     }, tag);
1062   }
1063
1064   @Nullable
1065   public static XmlElementDescriptor findXmlDescriptorByType(final XmlTag xmlTag) {
1066     return findXmlDescriptorByType(xmlTag, null);
1067   }
1068
1069   @Nullable
1070   public static XmlElementDescriptor findXmlDescriptorByType(final XmlTag xmlTag, @Nullable XmlTag context) {
1071     String type = xmlTag.getAttributeValue("type", XML_SCHEMA_INSTANCE_URI);
1072
1073     if (type == null) {
1074       String ns = xmlTag.getNamespace();
1075       if (XmlUtil.ourSchemaUrisList.indexOf(ns) >= 0) {
1076         type = xmlTag.getAttributeValue("type", null);
1077       }
1078     }
1079
1080     XmlElementDescriptor elementDescriptor = null;
1081     if (type != null) {
1082       final String namespaceByPrefix = findNamespaceByPrefix(findPrefixByQualifiedName(type), xmlTag);
1083       XmlNSDescriptor typeDecr = xmlTag.getNSDescriptor(namespaceByPrefix, true);
1084
1085       if (typeDecr == null && namespaceByPrefix.length() == 0) {
1086         if (context != null) typeDecr = context.getNSDescriptor("", true);
1087
1088         if (typeDecr == null) {
1089           final PsiFile containingFile = xmlTag.getContainingFile();
1090           if (containingFile instanceof XmlFile) {
1091             final XmlDocument document = ((XmlFile)containingFile).getDocument();
1092             if (document != null) typeDecr = (XmlNSDescriptor)document.getMetaData();
1093           }
1094         }
1095       }
1096
1097       if (typeDecr instanceof XmlNSDescriptorImpl) {
1098         final XmlNSDescriptorImpl schemaDescriptor = (XmlNSDescriptorImpl)typeDecr;
1099         elementDescriptor = schemaDescriptor.getDescriptorByType(type, xmlTag);
1100       }
1101     }
1102
1103     return elementDescriptor;
1104   }
1105
1106   public static boolean collectEnumerationValues(final XmlTag element, final HashSet<String> variants) {
1107     return processEnumerationValues(element, new Processor<XmlTag>() {
1108       public boolean process(XmlTag xmlTag) {
1109         variants.add(xmlTag.getAttributeValue(VALUE_ATTR_NAME));
1110         return true;
1111       }
1112     });
1113   }
1114
1115   public static boolean processEnumerationValues(final XmlTag element, final Processor<XmlTag> tagProcessor) {
1116     boolean exaustiveEnum = true;
1117
1118     for (final XmlTag tag : element.getSubTags()) {
1119       @NonNls final String localName = tag.getLocalName();
1120
1121       if (localName.equals(ENUMERATION_TAG_NAME)) {
1122         final String attributeValue = tag.getAttributeValue(VALUE_ATTR_NAME);
1123         if (attributeValue != null) tagProcessor.process(tag);
1124       }
1125       else if (localName.equals("union")) {
1126         exaustiveEnum = false;
1127         processEnumerationValues(tag, tagProcessor);
1128       }
1129       else if (!localName.equals("annotation")) {
1130         // don't go into annotation
1131         exaustiveEnum &= processEnumerationValues(tag, tagProcessor);
1132       }
1133     }
1134     return exaustiveEnum;
1135   }
1136
1137   public static XmlTag createChildTag(final XmlTag xmlTag,
1138                                       String localName,
1139                                       String namespace,
1140                                       String bodyText,
1141                                       boolean enforceNamespacesDeep) {
1142     String qname;
1143     final String prefix = xmlTag.getPrefixByNamespace(namespace);
1144     if (prefix != null && prefix.length() > 0) {
1145       qname = prefix + ":" + localName;
1146     }
1147     else {
1148       qname = localName;
1149     }
1150     try {
1151       @NonNls StringBuilder tagStartBuilder = StringBuilderSpinAllocator.alloc();
1152       String tagStart;
1153       try {
1154         tagStartBuilder.append(qname);
1155         if (xmlTag.getPrefixByNamespace(namespace) == null &&
1156             !(StringUtil.isEmpty(xmlTag.getNamespacePrefix()) && namespace.equals(xmlTag.getNamespace()))) {
1157           tagStartBuilder.append(" xmlns=\"");
1158           tagStartBuilder.append(namespace);
1159           tagStartBuilder.append("\"");
1160         }
1161         tagStart = tagStartBuilder.toString();
1162       }
1163       finally {
1164         StringBuilderSpinAllocator.dispose(tagStartBuilder);
1165       }
1166       Language language = xmlTag.getLanguage();
1167       if (!(language instanceof HTMLLanguage)) language = StdFileTypes.XML.getLanguage();
1168       XmlTag retTag;
1169       if (bodyText != null && bodyText.length() > 0) {
1170         retTag = XmlElementFactory.getInstance(xmlTag.getProject())
1171           .createTagFromText("<" + tagStart + ">" + bodyText + "</" + qname + ">", language);
1172         if (enforceNamespacesDeep) {
1173           retTag.acceptChildren(new XmlRecursiveElementVisitor() {
1174             @Override
1175             public void visitXmlTag(XmlTag tag) {
1176               final String namespacePrefix = tag.getNamespacePrefix();
1177               if (namespacePrefix.length() == 0) {
1178                 String qname;
1179                 if (prefix != null && prefix.length() > 0) {
1180                   qname = prefix + ":" + tag.getLocalName();
1181                 }
1182                 else {
1183                   qname = tag.getLocalName();
1184                 }
1185                 try {
1186                   tag.setName(qname);
1187                 }
1188                 catch (IncorrectOperationException e) {
1189                   LOG.error(e);
1190                 }
1191               }
1192               super.visitXmlTag(tag);
1193             }
1194           });
1195         }
1196       }
1197       else {
1198         retTag = XmlElementFactory.getInstance(xmlTag.getProject()).createTagFromText("<" + tagStart + "/>", language);
1199       }
1200       return retTag;
1201     }
1202     catch (IncorrectOperationException e) {
1203       LOG.error(e);
1204     }
1205     return null;
1206   }
1207
1208   @Nullable
1209   public static Pair<XmlTagChild, XmlTagChild> findTagChildrenInRange(final PsiFile file, int startOffset, int endOffset) {
1210     PsiElement elementAtStart = file.findElementAt(startOffset);
1211     PsiElement elementAtEnd = file.findElementAt(endOffset - 1);
1212     if (elementAtStart instanceof PsiWhiteSpace) {
1213       startOffset = elementAtStart.getTextRange().getEndOffset();
1214       elementAtStart = file.findElementAt(startOffset);
1215     }
1216     if (elementAtEnd instanceof PsiWhiteSpace) {
1217       endOffset = elementAtEnd.getTextRange().getStartOffset();
1218       elementAtEnd = file.findElementAt(endOffset - 1);
1219     }
1220     if (elementAtStart == null || elementAtEnd == null) return null;
1221
1222     XmlTagChild first = PsiTreeUtil.getParentOfType(elementAtStart, XmlTagChild.class);
1223     if (first == null) return null;
1224
1225     if (first.getTextRange().getStartOffset() != startOffset) {
1226       //Probably 'first' starts with whitespace
1227       PsiElement elementAt = file.findElementAt(first.getTextRange().getStartOffset());
1228       if (!(elementAt instanceof PsiWhiteSpace) || elementAt.getTextRange().getEndOffset() != startOffset) return null;
1229     }
1230
1231     XmlTagChild last = first;
1232     while (last != null && last.getTextRange().getEndOffset() < endOffset) {
1233       last = PsiTreeUtil.getNextSiblingOfType(last, XmlTagChild.class);
1234     }
1235
1236     if (last == null) return null;
1237     if (last.getTextRange().getEndOffset() != elementAtEnd.getTextRange().getEndOffset()) {
1238       //Probably 'last' ends with whitespace
1239       PsiElement elementAt = file.findElementAt(last.getTextRange().getEndOffset() - 1);
1240       if (!(elementAt instanceof PsiWhiteSpace) || elementAt.getTextRange().getStartOffset() != endOffset) {
1241         return null;
1242       }
1243     }
1244
1245     return new Pair<XmlTagChild, XmlTagChild>(first, last);
1246   }
1247
1248   public static boolean isSimpleXmlAttributeValue(final String unquotedValue, final XmlAttributeValue context) {
1249     for (int i = 0; i < unquotedValue.length(); ++i) {
1250       final char ch = unquotedValue.charAt(i);
1251       if (!Character.isJavaIdentifierPart(ch) && ch != ':' && ch != '-') {
1252         final XmlFile file = PsiTreeUtil.getParentOfType(context, XmlFile.class);
1253         if (file != null) return !tagFromTemplateFramework(file.getDocument().getRootTag());
1254         return false;
1255       }
1256     }
1257     return true;
1258   }
1259
1260   public static boolean toCode(String str) {
1261     for (int i = 0; i < str.length(); i++) {
1262       if (toCode(str.charAt(i))) return true;
1263     }
1264     return false;
1265   }
1266
1267   public static boolean toCode(char ch) {
1268     return "<&>\u00a0".indexOf(ch) >= 0;
1269   }
1270
1271   @Nullable
1272   public static PsiNamedElement findRealNamedElement(@NotNull final PsiNamedElement _element) {
1273     PsiElement currentElement = _element;
1274     final XmlEntityRef lastEntityRef = PsiTreeUtil.getParentOfType(currentElement, XmlEntityRef.class);
1275
1276     while (!(currentElement instanceof XmlFile)) {
1277       PsiElement dependingElement = currentElement.getUserData(XmlElement.DEPENDING_ELEMENT);
1278       if (dependingElement == null) dependingElement = currentElement.getContext();
1279       currentElement = dependingElement;
1280       if (dependingElement == null) break;
1281     }
1282
1283     if (currentElement instanceof XmlFile) {
1284       final String name = _element.getName();
1285       if (_element instanceof XmlEntityDecl) {
1286         final XmlEntityDecl cachedEntity = XmlEntityRefImpl.getCachedEntity((PsiFile)currentElement, name);
1287         if (cachedEntity != null) return cachedEntity;
1288       }
1289
1290       final PsiNamedElement[] result = new PsiNamedElement[1];
1291
1292       processXmlElements((XmlFile)currentElement, new PsiElementProcessor() {
1293         public boolean execute(final PsiElement element) {
1294           if (element instanceof PsiNamedElement) {
1295             final String elementName = ((PsiNamedElement)element).getName();
1296
1297             if (elementName.equals(name) && _element.getClass().isInstance(element)) {
1298               result[0] = (PsiNamedElement)element;
1299               return false;
1300             }
1301             else if (lastEntityRef != null &&
1302                      element instanceof XmlEntityDecl &&
1303                      elementName.equals(lastEntityRef.getText().substring(1, lastEntityRef.getTextLength() - 1))) {
1304               result[0] = (PsiNamedElement)element;
1305               return false;
1306             }
1307           }
1308
1309           return true;
1310         }
1311       }, true);
1312
1313       return result[0];
1314     }
1315
1316     return null;
1317   }
1318
1319   private static class MyAttributeInfo implements Comparable {
1320     boolean myRequired = true;
1321     String myName = null;
1322
1323     MyAttributeInfo(String name) {
1324       myName = name;
1325     }
1326
1327     MyAttributeInfo(String name, boolean flag) {
1328       myName = name;
1329       myRequired = flag;
1330     }
1331
1332     public int compareTo(Object o) {
1333       if (o instanceof MyAttributeInfo) {
1334         return myName.compareTo(((MyAttributeInfo)o).myName);
1335       }
1336       else if (o instanceof XmlAttribute) {
1337         return myName.compareTo(((XmlAttribute)o).getName());
1338       }
1339       return -1;
1340     }
1341   }
1342
1343   public static String generateDocumentDTD(XmlDocument doc) {
1344     final Map<String, List<String>> tags = new LinkedHashMap<String, List<String>>();
1345     final Map<String, List<MyAttributeInfo>> attributes = new LinkedHashMap<String, List<MyAttributeInfo>>();
1346
1347     try {
1348       XmlEntityRefImpl.setNoEntityExpandOutOfDocument(doc, true);
1349       final XmlTag rootTag = doc.getRootTag();
1350       computeTag(rootTag, tags, attributes);
1351
1352       // For supporting not welformed XML
1353       for (PsiElement element = rootTag != null ? rootTag.getNextSibling() : null; element != null; element = element.getNextSibling()) {
1354         if (element instanceof XmlTag) {
1355           computeTag((XmlTag)element, tags, attributes);
1356         }
1357       }
1358     }
1359     finally {
1360       XmlEntityRefImpl.setNoEntityExpandOutOfDocument(doc, false);
1361     }
1362
1363     final StringBuilder buffer = new StringBuilder();
1364     for (final String tagName : tags.keySet()) {
1365       buffer.append(generateElementDTD(tagName, tags.get(tagName), attributes.get(tagName)));
1366     }
1367     return buffer.toString();
1368   }
1369
1370   public static String generateElementDTD(String name, List<String> tags, List<MyAttributeInfo> attributes) {
1371     if (name == null || "".equals(name)) return "";
1372     if (name.contains(CompletionUtil.DUMMY_IDENTIFIER_TRIMMED)) return "";
1373
1374     @NonNls final StringBuilder buffer = StringBuilderSpinAllocator.alloc();
1375     try {
1376       buffer.append("<!ELEMENT ").append(name).append(" ");
1377       if (tags.isEmpty()) {
1378         buffer.append("(#PCDATA)>\n");
1379       }
1380       else {
1381         buffer.append("(");
1382         final Iterator<String> iter = tags.iterator();
1383         while (iter.hasNext()) {
1384           final String tagName = iter.next();
1385           buffer.append(tagName);
1386           if (iter.hasNext()) {
1387             buffer.append("|");
1388           }
1389           else {
1390             buffer.append(")*");
1391           }
1392         }
1393         buffer.append(">\n");
1394       }
1395       if (!attributes.isEmpty()) {
1396         buffer.append("<!ATTLIST ").append(name);
1397         for (final MyAttributeInfo info : attributes) {
1398           buffer.append("\n    ").append(generateAttributeDTD(info));
1399         }
1400         buffer.append(">\n");
1401       }
1402       return buffer.toString();
1403     }
1404     finally {
1405       StringBuilderSpinAllocator.dispose(buffer);
1406     }
1407   }
1408
1409   private static String generateAttributeDTD(MyAttributeInfo info) {
1410     if (info.myName.contains(CompletionUtil.DUMMY_IDENTIFIER_TRIMMED)) return "";
1411     @NonNls final StringBuilder buffer = StringBuilderSpinAllocator.alloc();
1412     try {
1413       buffer.append(info.myName).append(" ");
1414       //if ("id".equals(info.myName)) {
1415       //  buffer.append("ID");
1416       //}
1417       //else if ("ref".equals(info.myName)) {
1418       //  buffer.append("IDREF");
1419       //} else {
1420       buffer.append("CDATA");
1421       //}
1422       if (info.myRequired) {
1423         buffer.append(" #REQUIRED");
1424       }
1425       else {
1426         buffer.append(" #IMPLIED");
1427       }
1428       return buffer.toString();
1429     }
1430     finally {
1431       StringBuilderSpinAllocator.dispose(buffer);
1432     }
1433   }
1434
1435   @Nullable
1436   public static String trimLeadingSpacesInMultilineTagValue(@NonNls String tagValue) {
1437     return tagValue == null ? null : tagValue.replaceAll("\n\\s*", "\n");
1438   }
1439
1440   public static String findNamespaceByPrefix(final String prefix, XmlTag contextTag) {
1441     return contextTag.getNamespaceByPrefix(prefix);
1442   }
1443
1444   public static String findPrefixByQualifiedName(String name) {
1445     final int prefixEnd = name.indexOf(':');
1446     if (prefixEnd > 0) {
1447       return name.substring(0, prefixEnd);
1448     }
1449     return "";
1450   }
1451
1452   @Nullable
1453   public static String findLocalNameByQualifiedName(String name) {
1454     return name == null ? null : name.substring(name.indexOf(':') + 1);
1455   }
1456
1457
1458   public static XmlFile getContainingFile(PsiElement element) {
1459     while (!(element instanceof XmlFile) && element != null) {
1460       final PsiElement context = element.getContext();
1461       if (context == null) {
1462         //todo Dmitry Avdeev: either XmlExtension should work on any PsiFile (not just XmlFile), or you need to handle elements from JspJavaFile in some other way
1463         final XmlExtension extension = XmlExtension.getExtensionByElement(element);
1464         if (extension != null) {
1465           element = extension.getContainingFile(element);
1466         }
1467       }
1468       else {
1469         if (element == context) {
1470           LOG.error("Context==element: " + element.getClass());
1471           return null;
1472         }
1473         element = context;
1474       }
1475     }
1476     return (XmlFile)element;
1477   }
1478
1479   @Nullable
1480   public static String getSubTagValue(XmlTag tag, final String subTagName) {
1481     final XmlTag subTag = tag.findFirstSubTag(subTagName);
1482     if (subTag != null) {
1483       return subTag.getValue().getTrimmedText();
1484     }
1485     return null;
1486   }
1487
1488   public static int getStartOffsetInFile(XmlTag xmlTag) {
1489     int off = 0;
1490     while (true) {
1491       off += xmlTag.getStartOffsetInParent();
1492       final PsiElement parent = xmlTag.getParent();
1493       if (!(parent instanceof XmlTag)) break;
1494       xmlTag = (XmlTag)parent;
1495     }
1496     return off;
1497   }
1498
1499   public static XmlElement setNewValue(XmlElement tag, String value) throws IncorrectOperationException {
1500     if (tag instanceof XmlTag) {
1501       ((XmlTag)tag).getValue().setText(value);
1502       return tag;
1503     }
1504     else if (tag instanceof XmlAttribute) {
1505       XmlAttribute attr = (XmlAttribute)tag;
1506       attr.setValue(value);
1507       return attr;
1508     }
1509     else {
1510       throw new IncorrectOperationException();
1511     }
1512   }
1513
1514   public static String decode(@NonNls String text) {
1515     if (text.length() == 0) return text;
1516     if (text.charAt(0) != '&' || text.length() < 3) {
1517       if (text.indexOf('<') < 0 && text.indexOf('>') < 0) return text;
1518       return text.replaceAll("<!\\[CDATA\\[", "").replaceAll("\\]\\]>", "");
1519     }
1520
1521     if (text.equals("&lt;")) {
1522       return "<";
1523     }
1524     if (text.equals("&gt;")) {
1525       return ">";
1526     }
1527     if (text.equals("&nbsp;")) {
1528       return "\u00a0";
1529     }
1530     if (text.equals("&amp;")) {
1531       return "&";
1532     }
1533     if (text.equals("&apos;")) {
1534       return "'";
1535     }
1536     if (text.equals("&quot;")) {
1537       return "\"";
1538     }
1539     if (text.startsWith("&quot;") && text.endsWith("&quot;")) {
1540       return "\"" + text.substring(6, text.length() - 6) + "\"";
1541     }
1542     if (text.startsWith("&#")) {
1543       text = text.substring(3, text.length() - 1);
1544       try {
1545         return String.valueOf((char)Integer.parseInt(text));
1546       }
1547       catch (NumberFormatException e) {
1548         // ignore
1549       }
1550     }
1551
1552     return text;
1553   }
1554
1555   public static String unescape(String text) {
1556     return StringUtil.unescapeXml(text);
1557   }
1558
1559   public static String escape(String text) {
1560     return StringUtil.escapeXml(text);
1561   }
1562
1563   @NonNls private static final byte[] XML_PROLOG_START_BYTES = CharsetToolkit.getUtf8Bytes("<?xml");
1564   @NonNls private static final byte[] ENCODING_BYTES = CharsetToolkit.getUtf8Bytes("encoding");
1565   @NonNls private static final byte[] XML_PROLOG_END_BYTES = CharsetToolkit.getUtf8Bytes("?>");
1566
1567   @Nullable
1568   public static String extractXmlEncodingFromProlog(final byte[] content) {
1569     return detect(content);
1570   }
1571
1572   @Nullable
1573   private static String detect(final byte[] bytes) {
1574     int index = 0;
1575     if (CharsetToolkit.hasUTF8Bom(bytes)) {
1576       index = CharsetToolkit.UTF8_BOM.length;
1577     }
1578
1579     index = skipWhiteSpace(index, bytes);
1580     if (!ArrayUtil.startsWith(bytes, index, XML_PROLOG_START_BYTES)) return null;
1581     index += XML_PROLOG_START_BYTES.length;
1582     while (index < bytes.length) {
1583       index = skipWhiteSpace(index, bytes);
1584       if (ArrayUtil.startsWith(bytes, index, XML_PROLOG_END_BYTES)) return null;
1585       if (ArrayUtil.startsWith(bytes, index, ENCODING_BYTES)) {
1586         index += ENCODING_BYTES.length;
1587         index = skipWhiteSpace(index, bytes);
1588         if (index >= bytes.length || bytes[index] != '=') continue;
1589         index++;
1590         index = skipWhiteSpace(index, bytes);
1591         if (index >= bytes.length || bytes[index] != '\'' && bytes[index] != '\"') continue;
1592         byte quote = bytes[index];
1593         index++;
1594         StringBuilder encoding = new StringBuilder();
1595         while (index < bytes.length) {
1596           if (bytes[index] == quote) return encoding.toString();
1597           encoding.append((char)bytes[index++]);
1598         }
1599       }
1600       index++;
1601     }
1602     return null;
1603   }
1604
1605   private static int skipWhiteSpace(int start, final byte[] bytes) {
1606     while (start < bytes.length) {
1607       char c = (char)bytes[start];
1608       if (!Character.isWhitespace(c)) break;
1609       start++;
1610     }
1611     return start;
1612   }
1613
1614   @Nullable
1615   public static String extractXmlEncodingFromProlog(String text) {
1616     return detect(CharsetToolkit.getUtf8Bytes(text));
1617   }
1618
1619   public static void registerXmlAttributeValueReferenceProvider(PsiReferenceRegistrar registrar,
1620                                                                 @Nullable @NonNls String[] attributeNames,
1621                                                                 @Nullable ElementFilter elementFilter,
1622                                                                 @NotNull PsiReferenceProvider provider) {
1623     registerXmlAttributeValueReferenceProvider(registrar, attributeNames, elementFilter, true, provider);
1624   }
1625
1626   public static void registerXmlAttributeValueReferenceProvider(PsiReferenceRegistrar registrar,
1627                                                                 @Nullable @NonNls String[] attributeNames,
1628                                                                 @Nullable ElementFilter elementFilter,
1629                                                                 boolean caseSensitive,
1630                                                                 @NotNull PsiReferenceProvider provider) {
1631     registerXmlAttributeValueReferenceProvider(registrar, attributeNames, elementFilter, caseSensitive, provider,
1632                                                PsiReferenceRegistrar.DEFAULT_PRIORITY);
1633   }
1634
1635   public static void registerXmlAttributeValueReferenceProvider(PsiReferenceRegistrar registrar,
1636                                                                 @Nullable @NonNls String[] attributeNames,
1637                                                                 @Nullable ElementFilter elementFilter,
1638                                                                 boolean caseSensitive,
1639                                                                 @NotNull PsiReferenceProvider provider,
1640                                                                 double priority) {
1641     if (attributeNames == null) {
1642       registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue().and(new FilterPattern(elementFilter)), provider, priority);
1643       return;
1644     }
1645
1646     final StringPattern namePattern = caseSensitive
1647                                       ? StandardPatterns.string().oneOf(attributeNames)
1648                                       : StandardPatterns.string().oneOfIgnoreCase(attributeNames);
1649     registrar
1650       .registerReferenceProvider(XmlPatterns.xmlAttributeValue().withLocalName(namePattern).and(new FilterPattern(elementFilter)), provider,
1651                                  priority);
1652   }
1653
1654   public static void registerXmlTagReferenceProvider(PsiReferenceRegistrar registrar,
1655                                                      @NonNls String[] names,
1656                                                      @Nullable ElementFilter elementFilter,
1657                                                      boolean caseSensitive,
1658                                                      @NotNull PsiReferenceProvider provider) {
1659     if (names == null) {
1660       registrar.registerReferenceProvider(XmlPatterns.xmlTag().and(new FilterPattern(elementFilter)), provider,
1661                                           PsiReferenceRegistrar.DEFAULT_PRIORITY);
1662       return;
1663     }
1664
1665
1666     final StringPattern namePattern =
1667       caseSensitive ? StandardPatterns.string().oneOf(names) : StandardPatterns.string().oneOfIgnoreCase(names);
1668     registrar.registerReferenceProvider(XmlPatterns.xmlTag().withLocalName(namePattern).and(new FilterPattern(elementFilter)), provider,
1669                                         PsiReferenceRegistrar.DEFAULT_PRIORITY);
1670   }
1671
1672   public interface DuplicationInfoProvider<T extends PsiElement> {
1673     @Nullable
1674     String getName(@NotNull T t);
1675
1676     @NotNull
1677     String getNameKey(@NotNull T t, @NotNull String name);
1678
1679     @NotNull
1680     PsiElement getNodeForMessage(@NotNull T t);
1681   }
1682 }