blob: 650b54058f0c8280d9ee8d0838a58788c5bd5e94 [file] [log] [blame]
/*
* 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.xml.schema.completion.util;
import java.net.URI;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.swing.text.AbstractDocument;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenId;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.xml.lexer.XMLTokenId;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.xml.axi.*;
import org.netbeans.modules.xml.axi.datatype.Datatype;
import org.netbeans.modules.xml.schema.completion.*;
import org.netbeans.modules.xml.schema.completion.spi.CompletionModelProvider.CompletionModel;
import org.netbeans.modules.xml.schema.model.Form;
import org.netbeans.modules.xml.schema.model.GlobalElement;
import org.netbeans.modules.xml.schema.model.Import;
import org.netbeans.modules.xml.schema.model.Include;
import org.netbeans.modules.xml.schema.model.SchemaComponent;
import org.netbeans.modules.xml.schema.model.SchemaModel;
import org.netbeans.modules.xml.schema.model.SchemaModelReference;
import org.netbeans.modules.xml.schema.model.visitor.FindSubstitutions;
import org.netbeans.modules.xml.xam.locator.CatalogModelException;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
/**
* Utility class containing methods to query the model for completion suggestions.
*
* @author Samaresh (Samaresh.Panda@Sun.Com)
*/
public class CompletionUtil {
public static final String
TAG_FIRST_CHAR = "<", //NOI18N
TAG_LAST_CHAR = ">", //NOI18N
END_TAG_PREFIX = "</", //NOI18N
END_TAG_SUFFIX = "/>"; //NOI18N
// Pattern: ("<" + ("blank space" or "\n") + "any characters")
// ("<" + "/" + ("blank space" or "\n") + "any characters")
public static final Pattern PATTERN_TEXT_TAG_EOLs = Pattern.compile("</?[\\s]+.*");
private static final Logger _logger = Logger.getLogger(CompletionUtil.class.getName());
public static boolean noCompletion(JTextComponent target) {
if (target == null || target.getCaret() == null) {
return false;
}
int offset = target.getCaret().getDot();
if (offset < 0) {
return false;
}
//no completion inside CDATA or comment section
BaseDocument document = (BaseDocument) target.getDocument();
((AbstractDocument) document).readLock();
try {
TokenHierarchy th = TokenHierarchy.get(document);
TokenSequence ts = th.tokenSequence();
if (ts == null) {
return false;
}
ts.move(offset);
Token token = ts.token();
if (token == null) {
ts.moveNext();
token = ts.token();
if (token == null) {
return false;
}
}
if (token.id() == XMLTokenId.CDATA_SECTION || token.id() == XMLTokenId.BLOCK_COMMENT || token.id() == XMLTokenId.PI_START || token.id() == XMLTokenId.PI_END || token.id() == XMLTokenId.PI_CONTENT || token.id() == XMLTokenId.PI_TARGET) {
return true;
}
} finally {
((AbstractDocument) document).readUnlock();
}
return false;
}
/**
* No instantiation.
*/
private CompletionUtil() {
}
/**
* For debugging purposes only.
*/
public static void printPath(List<QName> path) {
StringBuffer buffer = new StringBuffer();
for(QName item: path) {
if(buffer.toString().equals(""))
buffer.append(item);
else
buffer.append("/" + item);
}
//System.out.println(buffer);
}
public static boolean isRoot(String tag, CompletionModel cm) {
if(cm == null)
return false;
AXIModel model = AXIModelFactory.getDefault().getModel(cm.getSchemaModel());
for(AbstractElement element : model.getRoot().getChildElements()) {
if(tag.endsWith(element.getName()))
return true;
}
return false;
}
/**
* Returns the prefix from the element's tag.
*/
public static String getPrefixFromTag(String tagName) {
if (tagName == null) return null;
int index = tagName.indexOf(":");
if (index == -1) return null;
String prefixName = tagName.substring(0, index);
if (prefixName.startsWith(END_TAG_PREFIX)) {
prefixName = prefixName.substring(END_TAG_PREFIX.length());
} else if (prefixName.startsWith(TAG_FIRST_CHAR)) {
prefixName = prefixName.substring(TAG_FIRST_CHAR.length());
}
return prefixName;
}
/**
* Returns the local name from the element's tag.
*/
public static String getLocalNameFromTag(String tagName) {
if (tagName == null) return null;
return (tagName.indexOf(":") == -1) ? tagName :
tagName.substring(tagName.indexOf(":") + 1, tagName.length());
}
/**
* Returns any prefix declared with this namespace. For example, if
* the namespace was declared as xmlns:po, the prefix 'po' will be returned.
* Returns null for declaration that contains no prefix.
*/
public static String getPrefixFromXMLNS(String namespace) {
if (namespace == null) return null;
return (namespace.indexOf(":") == -1) ? null :
namespace.substring(namespace.indexOf(":") + 1);
}
/**
* Returns the list of prefixes declared against the specified
* namespace. For example a document may declare namespaces as follows:
* xmlns="tns" xmlns:a="tns" xmlns:b="tns"
* The returned list in this case will be [a, b, null]
*/
public static List<String> getPrefixesAgainstNamespace(
CompletionContextImpl context, String namespace) {
List<String> list = new ArrayList<String>();
for(Map.Entry<String, String> entry : context.getDeclaredNamespaces().entrySet()) {
String key = entry.getKey();
String ns = entry.getValue();
if(ns.equals(namespace))
list.add(getPrefixFromXMLNS(key));
}
return list;
}
/**
* Populates schema URIs from schemaLocation and noNamespaceSchemaLocation
* attributes of the doc-root. For schemaLocation, uses the 2nd token, where as
* for the later, uses every token.
*/
public static void loadSchemaURIs(String schemaLocation, List<URI> uris, Map<String, String> schemaLocationMap) {
StringTokenizer st = new StringTokenizer(
schemaLocation.replace("\n", " "), " "); //NOI18N
while(st.hasMoreTokens()) {
URI uri = null;
try {
String token1 = st.nextToken().trim();
if(schemaLocationMap == null) {
uri = URI.create(token1); //every token is a schema
if(uri != null)
uris.add(uri);
continue;
}
if(st.hasMoreTokens()) {
String token2 = st.nextToken().trim();
uri = URI.create(token2); //every 2nd token is a schema
if(uri != null)
uris.add(uri);
schemaLocationMap.put(token1, token2);
}
} catch (Exception ex) {
continue;
}
}
}
private static AXIComponent findOriginal(AXIComponent c) {
while (true) {
switch (c.getComponentType()) {
case REFERENCE:
c = c.getSharedComponent();
break;
case PROXY:
c = c.getOriginal();
break;
default:
return c;
}
}
}
/**
* Returns the list of attributes for a given element.
*/
public static List<CompletionResultItem> getAttributes(
CompletionContextImpl context) {
Element element = findAXIElementAtContext(context);
if(element == null)
return null;
List<CompletionResultItem> results = new ArrayList<CompletionResultItem>();
for(AbstractAttribute aa: element.getAttributes()) {
AXIComponent original = findOriginal(aa);
if(original.getTargetNamespace() == null) { //no namespace
CompletionResultItem item = createResultItem(original, null, context);
if(item != null)
results.add(item);
continue;
}
if(original instanceof AnyAttribute) {
results.addAll(substituteAny((AnyAttribute)original, context));
continue;
}
addNSAwareCompletionItems(original,context,null,results);
}
return results;
}
/**
* Returns the list of child elements for a given element. This includes
* all available substitutions for child elements from all
* {@link CompletionModel}s in the specified completion context. It excludes
* abstract elements
* @param context the completion context for which to find all valid
* elements for insertion
* @return all elements appropriate to be inserted in the specified context,
* or {@code null} if no parent element can be found for the specified
* context.
*/
public static List<CompletionResultItem> getElements(CompletionContextImpl context) {
Element element = findAXIElementAtContext(context);
if(element == null) {
return null;
}
List<CompletionResultItem> results = new ArrayList<CompletionResultItem>();
for(AbstractElement ae: element.getChildElements()) {
AXIComponent original = findOriginal(ae);
if(original.getTargetNamespace() == null) { //no namespace
CompletionResultItem item = createResultItem(original, null, context);
if(item != null) {
results.add(item);
}
} else if(original instanceof AnyElement) {
results.addAll(substituteAny((AnyElement)original, context));
} else if(original instanceof Element) {
Element childElement = (Element) original;
if(!childElement.getAbstract()) {
addNSAwareCompletionItems(original, context, null, results);
}
addSubstitutionCompletionItems(childElement, context, results);
}
}
return results;
}
public static List<CompletionResultItem> getElementValues(
CompletionContextImpl context) {
Element element = findAXIElementAtContext(context);
List<CompletionResultItem> result = new ArrayList<CompletionResultItem>();
if(element == null)
return null;
AXIType type = element.getType();
if( type == null || !(type instanceof Datatype) ||
((Datatype)type).getEnumerations() == null)
return null;
for(Object value: ((Datatype)type).getEnumerations()) {
if(context.getTypedChars() == null || context.getTypedChars().equals("")) {
ValueResultItem item = new ValueResultItem(element, (String)value, context);
result.add(item);
continue;
}
String str = (String)value;
if(str.startsWith(context.getTypedChars())) {
ValueResultItem item = new ValueResultItem(element, (String)value, context);
result.add(item);
}
}
return result;
}
public static List<CompletionResultItem> getAttributeValues(
CompletionContextImpl context) {
Element element = findAXIElementAtContext(context);
if(element == null)
return null;
List<CompletionResultItem> result = new ArrayList<CompletionResultItem>();
Attribute attr = null;
for(AbstractAttribute a: element.getAttributes()) {
if(a instanceof AnyAttribute)
continue;
if(a.getName().equals(context.getAttribute())) {
attr = (Attribute)a;
break;
}
}
if(attr == null)
return null;
AXIType type = attr.getType();
if(type == null || !(type instanceof Datatype) ||
((Datatype)type).getEnumerations() == null)
return null;
for(Object value: ((Datatype)type).getEnumerations()) {
String str = (value != null) ? value.toString() : null;
if(context.getTypedChars() == null || context.getTypedChars().equals("")) {
ValueResultItem item = new ValueResultItem(attr, str, context);
result.add(item);
continue;
}
if (str != null && str.startsWith(context.getTypedChars())) {
ValueResultItem item = new ValueResultItem(attr, str, context);
result.add(item);
}
}
return result;
}
/**
* Find all possible substitutions for the specified {@link Element} within
* the specified {@link CompletionContextImpl completion context}.
* @param forSubstitution the Element to find substitutions for.
* @param context the context to use to find available substitutions. All
* available {@link CompletionModel}s' {@link SchemaModel}s will be searched.
* @param results the result set to add the results to.
*/
private static void addSubstitutionCompletionItems(Element forSubstitution, CompletionContextImpl context, List<CompletionResultItem> results) {
AXIModel model = forSubstitution.getModel();
String nsUri = forSubstitution.getTargetNamespace();
String localName = forSubstitution.getName();
for (CompletionModel completionModel : context.getCompletionModels()) {
SchemaModel schemaModel = completionModel.getSchemaModel();
Set<GlobalElement> substitutions = FindSubstitutions.resolveSubstitutions(schemaModel, nsUri, localName);
for (GlobalElement substitution : substitutions) {
AXIComponent substitutionElement = getAxiComponent(model, substitution);
addNSAwareCompletionItems(substitutionElement, context, completionModel, results);
}
}
}
/**
* Finds the {@link AXIComponent} representing the specified
* {@link SchemaComponent}, from the specified {@link AXIModel}.
* Modified from org.netbeans.modules.xml.axi.impl.Util.lookup(),
* org.netbeans.modules.xml.axi.impl.AXIModelImpl.lookup(),
* and org.netbeans.modules.xml.axi.impl.AXIModelImpl.lookupFromOtherModel().
* @param model the model to search in to find the representation of the
* specified {@link SchemaComponent}
* @param schemaComponent the {@link SchemaComponent} to search for a
* representation of
* @return the AXI representation of the specified schema component, or null
* if no AXI representation could be found for the schema component.
*/
private static AXIComponent getAxiComponent(AXIModel model, SchemaComponent schemaComponent) {
if(model.getSchemaModel() == schemaComponent.getModel()) {
return findChild(model.getRoot(), schemaComponent);
}
if(!schemaComponent.isInDocumentModel()) {
return null;
}
AXIModelFactory factory = AXIModelFactory.getDefault();
AXIModel otherModel = factory.getModel(schemaComponent.getModel());
return otherModel == null ? null : findChild(otherModel.getRoot(), schemaComponent);
}
/**
* Finds an {@link AXIComponent} representation of the specified
* {@link SchemaComponent}, by searching the children of the specified
* {@link AXIDocument}.
* Adapted from org.netbeans.modules.xml.axi.impl.AXIDocumentImpl.findChild().
* @param document the document to search through to find the representation
* of the specified schema component
* @param child the schema component whose representation to find in the
* specified document
* @return the AXI representation of the specified schema component, from
* the specified document, or {@code null} if the schema component has no
* representation in the children of the document.
*/
private static AXIComponent findChild(AXIDocument document, SchemaComponent child) {
for(AXIComponent childRepresentation : document.getChildren()) {
if(childRepresentation.getPeer() == child) {
return childRepresentation;
}
}
return null;
}
private static void addNSAwareCompletionItems(AXIComponent axi,
CompletionContextImpl context, CompletionModel cm, List<CompletionResultItem> results) {
String typedChars = context.getTypedChars();
CompletionResultItem item = null;
if (! isFormQualified(axi)) {
item = createResultItem(axi, null, context);
if (item == null)
return;
if (typedChars == null) {
results.add(item);
} else if (isResultItemTextStartsWith(item, typedChars)) {
results.add(item);
}
return;
}
// namespace aware items
List<String> prefixes = getPrefixes(context, axi, cm);
if (prefixes.size() == 0) {
prefixes.add(null);
}
for (String prefix: prefixes) {
item = createResultItem(axi, prefix, context);
if (item == null)
continue;
if (typedChars == null) {
results.add(item);
} else if (isResultItemTextStartsWith(item, typedChars)) {
results.add(item);
}
}
}
private static boolean isResultItemTextStartsWith(CompletionResultItem resultItem,
String text) {
if ((resultItem == null) || (text == null)) return false;
String resultText = resultItem.getReplacementText();
int startIndex = 0;
if (resultText.startsWith(END_TAG_PREFIX) && (! text.startsWith(END_TAG_PREFIX))) {
startIndex = END_TAG_PREFIX.length();
} else if (resultText.startsWith(TAG_FIRST_CHAR) &&
(! text.startsWith(TAG_FIRST_CHAR))) {
startIndex = TAG_FIRST_CHAR.length();
}
boolean result = resultText.startsWith(text, startIndex);
return result;
}
private static CompletionResultItem createResultItem(AXIComponent axi,
String prefix, CompletionContextImpl context) {
CompletionResultItem item = null;
if(axi instanceof AbstractElement) {
if(prefix == null)
item = new ElementResultItem((AbstractElement)axi, context);
else
item = new ElementResultItem((AbstractElement)axi, prefix, context);
}
if(axi instanceof AbstractAttribute) {
Attribute a = (Attribute)axi;
if(prefix == null) {
if(!context.getExistingAttributes().contains(a.getName()))
item = new AttributeResultItem((AbstractAttribute)axi, context);
} else {
if(!context.getExistingAttributes().contains(prefix+":"+a.getName()))
item = new AttributeResultItem((AbstractAttribute)axi, prefix, context);
}
}
return item;
}
private static List<String> getPrefixes(CompletionContextImpl context, AXIComponent ae,
CompletionModel cm) {
List<String> prefixes = new ArrayList<String>();
if(cm == null) {
ae = findOriginal(ae);
String defaultNS = context.getDefaultNamespace();
String targetNS = ae.getTargetNamespace();
if((defaultNS != targetNS) && // in case both are null, go away
(defaultNS == null || targetNS == null ||
!context.getDefaultNamespace().equals(ae.getTargetNamespace()))) {
prefixes = getPrefixesAgainstNamespace(context, ae.getTargetNamespace());
if(prefixes.size() != 0)
return prefixes;
String prefix = context.suggestPrefix(ae.getTargetNamespace());
CompletionModel m = new CompletionModelEx(context, prefix,
ae.getModel().getSchemaModel());
context.addCompletionModel(m);
prefixes.add(prefix); //NOI18N
return prefixes;
}
return getPrefixesAgainstNamespace(context, ae.getTargetNamespace());
}
prefixes = getPrefixesAgainstNamespace(context, cm.getTargetNamespace());
if(prefixes.size() == 0)
prefixes.add(cm.getSuggestedPrefix());
return prefixes;
}
private static boolean isFormQualified(AXIComponent component) {
if(component instanceof Attribute) {
AXIComponent original = component.getOriginal();
if( ((Attribute)original).isReference() ||
(original.getParent() instanceof AXIDocument) )
return true;
Attribute a = (Attribute)component;
return (a.getForm() == Form.QUALIFIED);
}
if(component instanceof Element) {
AXIComponent original = component.getOriginal();
if( ((Element)original).isReference() ||
(original.getParent() instanceof AXIDocument) )
return true;
Element e = (Element)component;
return (e.getForm() == Form.QUALIFIED);
}
return false;
}
/**
* Returns the appropriate AXIOM element for a given context.
*/
public static Element findAXIElementAtContext(
CompletionContextImpl context) {
List<QName> path = context.getPathFromRoot();
if(path == null || path.size() == 0)
return null;
CompletionModel cm = null;
QName tag = context.getPathFromRoot().get(0);
String tns = tag.getNamespaceURI();
if(tns != null && tns.equals(XMLConstants.NULL_NS_URI)) {
cm = context.getActiveNoNSModel();
} else {
cm = context.getCompletionModelMap().get(tns);
}
if(cm == null)
return null;
AXIModel am = AXIModelFactory.getDefault().getModel(cm.getSchemaModel());
AXIComponent parent = am.getRoot();
if(parent == null)
return null;
AXIComponent child = null;
for(QName qname : path) {
child = findChildElement(parent, qname,context);
parent = child;
}
if(child != null && (child instanceof Element))
return (Element)child;
return null;
}
private static AXIComponent findChildElement(AXIComponent parent,
QName qname, CompletionContextImpl context) {
if(parent == null) {
return null;
}
for(AXIComponent element : parent.getChildElements()) {
if(!(element instanceof Element)) {
continue;
}
Element e = (Element)element;
if(qname.getLocalPart().equals(e.getName())) {
return element;
}
if(e.isReference()){
Element ref=e.getReferent();
// check substitutions
AXIModel model = ref.getModel();
String nsUri = ref.getTargetNamespace();
String localName = ref.getName();
for (CompletionModel completionModel : context.getCompletionModels()) {
SchemaModel schemaModel = completionModel.getSchemaModel();
Set<GlobalElement> substitutions = FindSubstitutions.resolveSubstitutions(schemaModel, nsUri, localName);
for (GlobalElement substitution : substitutions) {
AXIComponent substitutionElement = getAxiComponent(model, substitution);
if(substitutionElement instanceof Element){
if(((Element)substitutionElement).getName().equals(qname.getLocalPart())) {
return substitutionElement;
}
}
}
}
}
}
for (AXIComponent c : parent.getChildren()) {
if (c instanceof SchemaReference) {
SchemaReference ref = (SchemaReference)c;
SchemaModelReference in = (SchemaModelReference)ref.getPeer();
SchemaModel model;
try {
model = in.resolveReferencedModel();
AXIModel am = AXIModelFactory.getDefault().getModel(model);
AXIComponent check = findChildElement(am.getRoot(), qname, context);
if (check != null) {
return check;
}
} catch (CatalogModelException ex) {
// ignore
}
}
}
return null;
}
/**
* Substitue any or anyAttribute with a valid list of items.
*/
private static List<CompletionResultItem> substituteAny(AXIComponent any,
CompletionContextImpl context) {
List<CompletionResultItem> items = new ArrayList<CompletionResultItem>();
String anyNamespace = any.getTargetNamespace();
String tns = any.getModel().getRoot().getTargetNamespace();
for (CompletionModel cm : context.getCompletionModels()) {
if (cm == null) continue;
//##other => items from other namespaces
if (anyNamespace.equals("##other")) { //NOI18N
if ((tns != null) && (! tns.equals(cm.getTargetNamespace()))) {
populateItemsForAny(cm, any, context, items);
}
}
//##targetNamespace => items from target namespace
if(anyNamespace.equals("##targetNamespace")) { //NOI18N
if(tns != null && tns.equals(cm.getTargetNamespace()))
populateItemsForAny(cm,any,context,items);
}
//##local => unqualified items
if(anyNamespace.equals("##local") && //NOI18N
cm.getTargetNamespace() == null) {
populateItemsForAny(cm,any,context,items);
}
//only specfied namespaces
if(!anyNamespace.startsWith("##") && //NOI18N
cm.getTargetNamespace() != null &&
anyNamespace.indexOf(cm.getTargetNamespace()) != -1) {
populateItemsForAny(cm,any,context,items);
}
//##any => unconditional
if(anyNamespace.equals("##any")) { //NOI18N
populateItemsForAny(cm,any,context,items);
}
}
return items;
}
private static void populateItemsForAny(CompletionModel cm, AXIComponent any,
CompletionContextImpl context, List<CompletionResultItem> items) {
if (cm == null) return;
AXIModel am = AXIModelFactory.getDefault().getModel(cm.getSchemaModel());
if (any instanceof AnyElement) {
for(Element e : am.getRoot().getElements()) {
addNSAwareCompletionItems(e, context, cm, items);
}
}
if (any instanceof AnyAttribute) {
for(Attribute a : am.getRoot().getAttributes()) {
addNSAwareCompletionItems(a, context, cm, items);
}
}
}
/**
* Finds namespaces declared in all start tags in the document and keeps a map
* of namespaces to their prefixes.
* @param document
* @return
*/
public static HashMap<String, String> getNamespacesFromStartTags(Document document) {
HashMap<String, String> map = new HashMap<String, String>();
((AbstractDocument)document).readLock();
try {
TokenHierarchy th = TokenHierarchy.get(document);
TokenSequence ts = th.tokenSequence();
String lastNS = null;
while(ts.moveNext()) {
Token t = ts.token();
if(t.id() == XMLTokenId.ARGUMENT &&
t.text().toString().startsWith(XMLConstants.XMLNS_ATTRIBUTE)) {
lastNS = t.text().toString();
}
if(t.id() == XMLTokenId.VALUE && lastNS != null) {
String value = t.text().toString();
if(value.length() >= 2 && (value.startsWith("'") || value.startsWith("\""))) {
value = value.substring(1, value.length()-1);
}
map.put(value, CompletionUtil.getPrefixFromXMLNS(lastNS));
lastNS = null;
}
} //while loop
} finally {
((AbstractDocument)document).readUnlock();
}
return map;
}
// private static NsHandler getNamespaces(InputSource is) {
// NsHandler handler = new NsHandler();
// try {
// XMLReader xmlReader = org.openide.xml.XMLUtil.createXMLReader(false, true);
// xmlReader.setContentHandler(handler);
// xmlReader.parse(is);
// } catch (Exception ex) {
// logger.log(Level.INFO, ex.getMessage());
// }
// return handler;
// }
// private static class NsHandler extends org.xml.sax.helpers.DefaultHandler {
//
// //key=tns, value=prefix
// private HashMap<String, String> nsMap;
//
// NsHandler() {
// nsMap = new HashMap<String, String>();
// }
//
// @Override
// public void startElement(String uri, String localName, String rawName,
// Attributes atts) throws SAXException {
// if (atts.getLength() > 0) {
// String locations = atts.getValue(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI,
// "schemaLocation"); // NOI18N
// if (locations == null)
// return;
// StringTokenizer tokenizer = new StringTokenizer(locations);
// if ((tokenizer.countTokens() % 2) == 0) {
// while (tokenizer.hasMoreElements()) {
// String nsURI = tokenizer.nextToken();
// String nsLocation = tokenizer.nextToken();
// //mapping.put(nsURI, nsLocation);
// }
// }
// }
// }
//
// @Override
// public void startPrefixMapping(String prefix, String uri) throws SAXException {
// if (XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI.equals(uri)) {
// return;
// }
// nsMap.put(uri, prefix);
// }
//
// HashMap<String, String> getNamespaceMap() {
// return nsMap;
// }
// }
/**
* Checks to see if this document declares any DOCTYPE or not?
* If exists, it must appear before the first xml tag.
* @return true if found, else false.
*/
public static boolean isDTDBasedDocument(Document document) {
((AbstractDocument)document).readLock();
try {
TokenHierarchy th = TokenHierarchy.get(document);
TokenSequence ts = th.tokenSequence();
while(ts.moveNext()) {
Token token = ts.token();
//if an xml tag is found, we have come too far.
if(token.id() == XMLTokenId.TAG)
return false;
if(token.id() == XMLTokenId.DECLARATION)
return true;
}
} finally {
((AbstractDocument)document).readUnlock();
}
return false;
}
/**
* Finds the namespace insertion offset in the xml document.
*/
public static int getNamespaceInsertionOffset(Document document) {
int offset = 0;
((AbstractDocument)document).readLock();
try {
TokenHierarchy th = TokenHierarchy.get(document);
TokenSequence ts = th.tokenSequence();
while(ts.moveNext()) {
Token nextToken = ts.token();
if(nextToken.id() == XMLTokenId.TAG && nextToken.text().toString().equals(">")) {
offset = nextToken.offset(th);
break;
}
}
} finally {
((AbstractDocument)document).readUnlock();
}
return offset;
}
/**
* Finds the root element of the xml document. It also populates the
* attributes for the root element.
*
* See DocRoot.
*/
public static DocRoot getDocRoot(Document document) {
((AbstractDocument)document).readLock();
try {
TokenHierarchy th = TokenHierarchy.get(document);
TokenSequence ts = th.tokenSequence();
List<DocRootAttribute> attributes = new ArrayList<DocRootAttribute>();
String name = null;
while(ts.moveNext()) {
Token nextToken = ts.token();
if(nextToken.id() == XMLTokenId.TAG) {
String tagName = nextToken.text().toString();
if(name == null && tagName.startsWith("<"))
name = tagName.substring(1, tagName.length());
String lastAttrName = null;
while(ts.moveNext() ) {
Token t = ts.token();
if(t.id() == XMLTokenId.TAG && t.text().toString().equals(">"))
break;
if(t.id() == XMLTokenId.ARGUMENT) {
lastAttrName = t.text().toString();
}
if(t.id() == XMLTokenId.VALUE && lastAttrName != null) {
String value = t.text().toString();
if(value == null || value.length() == 1)
value = null;
else if(value.startsWith("'") || value.startsWith("\""))
value = value.substring(1, value.length()-1);
attributes.add(new DocRootAttribute(lastAttrName, value));
lastAttrName = null;
}
} //while loop
//first start tag with a valid name is the root
if(name != null)
break;
}
} //while loop
return new DocRoot(name, attributes);
} finally {
((AbstractDocument)document).readUnlock();
}
}
/**
* Can't provide completion for XML documents based on DTD
* and for those who do not declare namespaces in root tag.
*/
public static boolean canProvideCompletion(BaseDocument doc) {
FileObject file = getPrimaryFile(doc);
if(file == null)
return false;
//for .xml documents
if("xml".equals(file.getExt())) { //NOI18N
//if docroot doesn't declare ns, no completion
DocRoot root = CompletionUtil.getDocRoot(doc);
if(root != null && !root.declaresNamespace()) {
return false;
}
}
return true;
}
public static FileObject getPrimaryFile(Document doc) {
Object o = doc.getProperty(Document.StreamDescriptionProperty);
if (o instanceof FileObject) {
return (FileObject) o;
} else if (o instanceof Lookup.Provider) {
//Note: DataObject is a Lookup.Provider
return ((Lookup.Provider) o).getLookup().lookup(FileObject.class);
} else {
return null;
}
}
/*
private static StringBuilder getTokenSequenceDump(TokenSequence ts) {
StringBuilder sb = new StringBuilder();
ts.moveStart();
int i = 0;
while (ts.moveNext()) {
Token token = ts.token();
sb.append(i + ". [" + token.id() + ", " + ts.offset() + "] ==>\n");
int tokenLength = token.length();
String text = token.text().toString();
text = (text.trim().length() < 1 ? (tokenLength + " ws") : text);
sb.append(text);
sb.append("\n<==\n");
++i;
}
return sb;
}
*/
public static CompletionResultItem getEndTagCompletionItem(JTextComponent component,
BaseDocument document) {
int caretPos = component.getCaret().getDot();
try {
((AbstractDocument) document).readLock();
TokenHierarchy tokenHierarchy = TokenHierarchy.get(document);
TokenSequence tokenSequence = tokenHierarchy.tokenSequence();
String incompleteTagName = findIncompleteTagName(caretPos, tokenSequence);
if (isCaretInsideTag(caretPos, tokenSequence)) return null;
boolean beforeUnclosedStartTagFound = isUnclosedStartTagFoundBefore(
caretPos, tokenSequence);
if (! beforeUnclosedStartTagFound) return null;
Token token = tokenSequence.token();
String startTagName = getTokenTagName(token);
if (startTagName == null) return null;
boolean closingTagFound = isClosingEndTagFoundAfter(caretPos,
tokenSequence, startTagName);
if (closingTagFound) return null;
CompletionResultItem resultItem;
if ((incompleteTagName != null) &&
(! startTagName.startsWith(incompleteTagName))) {
resultItem = new TagLastCharResultItem(incompleteTagName, tokenSequence);
} else {
resultItem = new EndTagResultItem(startTagName, tokenSequence);
}
return resultItem;
} catch(Exception e) {
_logger.log(Level.WARNING,
e.getMessage() == null ? e.getClass().getName() : e.getMessage(), e);
return null;
} finally {
((AbstractDocument) document).readUnlock();
}
}
private static boolean isUnclosedStartTagFoundBefore(int caretPos,
TokenSequence tokenSequence) {
tokenSequence.move(caretPos);
boolean startTagFound = false, tagLastCharFound = false;
Stack<String> existingEndTags = new Stack<String>();
String startTagName, endTagName;
while (tokenSequence.movePrevious()) {
Token token = tokenSequence.token();
if (isTagLastChar(token)) {
tagLastCharFound = true;
} else if (isEndTagPrefix(token)) {
tagLastCharFound = startTagFound = false;
endTagName = getTokenTagName(token);
if (endTagName != null) {
existingEndTags.push(endTagName);
}
} else if (isTagFirstChar(token) && tagLastCharFound) {
startTagName = getTokenTagName(token);
endTagName = existingEndTags.isEmpty() ? null : existingEndTags.peek();
if ((startTagName != null) && (endTagName != null) &&
startTagName.equals(endTagName)) {
existingEndTags.pop();
tagLastCharFound = startTagFound = false;
continue;
}
startTagFound = true;
break;
}
}
return startTagFound;
}
private static String findIncompleteTagName(int caretPos, TokenSequence tokenSequence) {
if (! isTokenSequenceUsable(tokenSequence)) return null;
boolean tagFirstCharFound = false;
Token token = null;
String incompleteTagName = null;
int cut = tokenSequence.move(caretPos);
tokenSequence.moveNext();
boolean inMiddle = cut != 0;
do {
token = tokenSequence.token();
TokenId tokenID = token.id();
if (tokenID.equals(XMLTokenId.TAG) || tokenID.equals(XMLTokenId.TEXT)) {
String tokenText = token.text().toString();
if ((tokenText == null) || tokenText.isEmpty()) continue;
if (tokenText.startsWith(TAG_FIRST_CHAR)) {
if (tokenSequence.offset() < caretPos) {
tagFirstCharFound = true;
incompleteTagName = getTokenTagName(token);
break;
}
} else {
return null;
}
} else if (inMiddle && tokenID.equals(XMLTokenId.BLOCK_COMMENT)) {
}
inMiddle = false;
} while (tokenSequence.movePrevious());
if (! tagFirstCharFound) return null;
tokenSequence.move(caretPos);
while (tokenSequence.moveNext()) {
token = tokenSequence.token();
TokenId tokenID = token.id();
if (tokenID.equals(XMLTokenId.TAG)) {
String tokenText = token.text().toString();
if ((tokenText == null) || tokenText.isEmpty()) continue;
if (tokenText.contains(TAG_LAST_CHAR)) {
return null;
} else {
return incompleteTagName;
}
}
}
return incompleteTagName;
}
private static boolean isClosingEndTagFoundAfter(int caretPos,
TokenSequence tokenSequence, String startTagName) {
if ((tokenSequence == null) || (startTagName == null)) return false;
tokenSequence.move(caretPos);
int unclosedTagCount = 1;
while (tokenSequence.moveNext()) {
Token token = tokenSequence.token();
String nextTagName = getTokenTagName(token);
// fix for issue #185048
// (http://netbeans.org/bugzilla/show_bug.cgi?id=185048)
// also: must not count ends of nested tags, just the
// same level
if (isEndTagPrefix(token)) {
if (unclosedTagCount-- == 0) {
return false;
}
} else if (isTagFirstChar(token)) {
unclosedTagCount++;
} else {
continue;
}
if (unclosedTagCount == 0) {
if (isEndTagPrefix(token)) {
return startTagName.equals(nextTagName);
}
}
}
return false;
}
public static boolean isTokenSequenceUsable(TokenSequence tokenSequence) {
return ((tokenSequence != null) && (tokenSequence.isValid()) &&
(! tokenSequence.isEmpty()));
}
public static boolean isCaretInsideTag(int caretPos, TokenSequence tokenSequence) {
if (! isTokenSequenceUsable(tokenSequence)) return false;
boolean tagFirstCharFound = false, tagLastCharFound = false;
Token token = null;
boolean checkComment = tokenSequence.move(caretPos) != 0;
tokenSequence.moveNext();
do {
token = tokenSequence.token();
if (isTagFirstChar(token)) {
tagFirstCharFound = true;
break;
} else if (checkComment && token.id() == XMLTokenId.BLOCK_COMMENT) {
return true;
}
checkComment = false;
} while (tokenSequence.movePrevious());
if (! tagFirstCharFound) return false;
while (tokenSequence.moveNext()) {
token = tokenSequence.token();
int tokenOffset = tokenSequence.offset();
boolean isEndTagSuffix = isEndTagSuffix(token);
if (isTagLastChar(token) || isEndTagSuffix) {
if ((tokenOffset >= caretPos) ||
(isEndTagSuffix && (tokenOffset == caretPos - 1))) {
tagLastCharFound = true;
}
break;
}
if (token.id() == XMLTokenId.TAG || token.id() == XMLTokenId.TEXT) {
if (token.text().charAt(0) == '<') {
break;
}
}
}
return (tagFirstCharFound && tagLastCharFound);
}
public static boolean isTagFirstChar(Token token) {
if (token == null) return false;
TokenId tokenID = token.id();
if (tokenID.equals(XMLTokenId.TAG) || tokenID.equals(XMLTokenId.TEXT)) {
String tokenText = token.text().toString();
if ((! isEndTagPrefix(token)) && tokenText.startsWith(TAG_FIRST_CHAR)) {
return true;
}
}
return false;
}
public static boolean isTagLastChar(Token token) {
if (token == null) return false;
TokenId tokenID = token.id();
if (tokenID.equals(XMLTokenId.TAG)) {
String tokenText = token.text().toString();
if (tokenText.equals(TAG_LAST_CHAR)) {
return true;
}
}
return false;
}
public static boolean isEndTagPrefix(Token token) {
if (token == null) return false;
TokenId tokenID = token.id();
if (tokenID.equals(XMLTokenId.TAG) || tokenID.equals(XMLTokenId.TEXT)) {
String tokenText = token.text().toString();
if (tokenText.startsWith(END_TAG_PREFIX)) {
return true;
}
}
return false;
}
public static boolean isEndTagSuffix(Token token) {
if (token == null) return false;
TokenId tokenID = token.id();
if (tokenID.equals(XMLTokenId.TAG)) {
String tokenText = token.text().toString();
if (tokenText.equals(END_TAG_SUFFIX)) {
return true;
}
}
return false;
}
public static boolean isTextTag(Token token) {
if (token == null) return false;
TokenId tokenID = token.id();
if (! tokenID.equals(XMLTokenId.TEXT)) return false;
String tokenText = token.text().toString();
if (tokenText.equals("<")) {
return true;
}
boolean result = PATTERN_TEXT_TAG_EOLs.matcher(tokenText).matches();
return result;
}
public static String getTokenTagName(Token token) {
if (token == null) return null;
int index = -1;
if (isTagFirstChar(token)) {
index = TAG_FIRST_CHAR.length();
} else if (isEndTagPrefix(token)) {
index = END_TAG_PREFIX.length();
} else {
return null;
}
String tokenText = token.text().toString().substring(index).trim();
return tokenText.isEmpty() ? null : tokenText;
}
//========================================================================//
public static class DocRoot {
//name of the root along with prefix, e.g. po:purchaseOrder
private String name;
private List<DocRootAttribute> attributes;
DocRoot(String name, List<DocRootAttribute> attributes) {
this.name = name;
this.attributes = new ArrayList(attributes);
}
public String getName() {
return name;
}
public String getPrefix() {
return CompletionUtil.getPrefixFromTag(name);
}
public List<DocRootAttribute> getAttributes() {
return attributes;
}
public boolean declaresNamespace() {
for(DocRootAttribute attr: getAttributes()) {
if(attr.getName().startsWith(XMLConstants.XMLNS_ATTRIBUTE))
return true;
}
return false;
}
}
//========================================================================//
public static class DocRootAttribute {
private String name;
private String value;
DocRootAttribute(String name, String value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return name+"="+value;
}
}
}