# HG changeset patch # Parent cec51eb4fe3914d978e8a2be5ae635205ea6837e # User Daniel Bell Adds substitution group members to code completion diff --git a/xml.schema.completion/nbproject/project.xml b/xml.schema.completion/nbproject/project.xml --- a/xml.schema.completion/nbproject/project.xml +++ b/xml.schema.completion/nbproject/project.xml @@ -125,6 +125,7 @@ 1 + 1.17 diff --git a/xml.schema.completion/src/org/netbeans/modules/xml/schema/completion/util/CompletionUtil.java b/xml.schema.completion/src/org/netbeans/modules/xml/schema/completion/util/CompletionUtil.java --- a/xml.schema.completion/src/org/netbeans/modules/xml/schema/completion/util/CompletionUtil.java +++ b/xml.schema.completion/src/org/netbeans/modules/xml/schema/completion/util/CompletionUtil.java @@ -44,11 +44,7 @@ package org.netbeans.modules.xml.schema.completion.util; import java.net.URI; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Stack; -import java.util.StringTokenizer; +import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; @@ -63,27 +59,22 @@ import org.netbeans.api.lexer.TokenSequence; import org.netbeans.api.xml.lexer.XMLTokenId; import org.netbeans.editor.BaseDocument; -import org.netbeans.modules.xml.axi.AXIComponent; -import org.netbeans.modules.xml.axi.AXIDocument; -import org.netbeans.modules.xml.axi.AXIModel; -import org.netbeans.modules.xml.axi.AXIModelFactory; -import org.netbeans.modules.xml.axi.AXIType; -import org.netbeans.modules.xml.axi.AbstractAttribute; -import org.netbeans.modules.xml.axi.AbstractElement; -import org.netbeans.modules.xml.axi.AnyAttribute; -import org.netbeans.modules.xml.axi.AnyElement; -import org.netbeans.modules.xml.axi.Attribute; -import org.netbeans.modules.xml.axi.Element; +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.SchemaComponent; +import org.netbeans.modules.xml.schema.model.SchemaModel; +import org.netbeans.modules.xml.schema.model.visitor.FindSubstitutionsVisitor; import org.openide.filesystems.FileObject; import org.openide.loaders.DataObject; import org.openide.windows.TopComponent; /** - * + * Utility class containing methods to query the model for completion suggestions. + * * @author Samaresh (Samaresh.Panda@Sun.Com) */ public class CompletionUtil { @@ -243,28 +234,39 @@ } /** - * Returns the list of child-elements for a given element. + * 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 getElements( - CompletionContextImpl context) { + public static List getElements(CompletionContextImpl context) { Element element = findAXIElementAtContext(context); - if(element == null) + if(element == null) { return null; + } List results = new ArrayList(); for(AbstractElement ae: element.getChildElements()) { AXIComponent original = ae.getOriginal(); if(original.getTargetNamespace() == null) { //no namespace CompletionResultItem item = createResultItem(original, null, context); - if(item != null) + if(item != null) { results.add(item); - continue; - } - if(original instanceof AnyElement) { + } + } else if(original instanceof AnyElement) { results.addAll(substituteAny((AnyElement)original, context)); - continue; + } else if(original instanceof Element) { + Element childElement = (Element) original; + if(!childElement.getAbstract()) { + addNSAwareCompletionItems(original, context, null, results); + } + addSubstitutionCompletionItems(childElement, context, results); } - addNSAwareCompletionItems(original,context,null,results); } return results; } @@ -329,6 +331,76 @@ } 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 results) { + AXIModel model = forSubstitution.getModel(); + String nsUri = forSubstitution.getTargetNamespace(); + String localName = forSubstitution.getName(); + for (CompletionModel completionModel : context.getCompletionModels()) { + SchemaModel schemaModel = completionModel.getSchemaModel(); + Set substitutions = FindSubstitutionsVisitor.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 results) { diff --git a/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/AbstractElementTest.java b/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/AbstractElementTest.java new file mode 100644 --- /dev/null +++ b/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/AbstractElementTest.java @@ -0,0 +1,89 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2011 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU General + * Public License Version 2 only ("GPL") or the Common Development and + * Distribution License("CDDL") (collectively, the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy of the + * License at http://www.netbeans.org/cddl-gplv2.html or + * nbbuild/licenses/CDDL-GPL-2-CP. See the License for the specific language + * governing permissions and limitations under the License. When distributing + * the software, include this License Header Notice in each file and include the + * License file at nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided by Oracle + * in the GPL Version 2 section of the License file that accompanied this code. + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL or only + * the GPL Version 2, indicate your decision by adding "[Contributor] elects to + * include this software in this distribution under the [CDDL or GPL Version 2] + * license." If you do not indicate a single choice of license, a recipient has + * the option to distribute your version of this file under either the CDDL, the + * GPL Version 2 or to extend the choice of license to its licensees as provided + * above. However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is made + * subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2011 Sun Microsystems, Inc. + */ +package org.netbeans.modules.xml.schema.completion; + +import java.util.List; +import junit.framework.Test; +import junit.framework.TestSuite; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; + +/** + * Tests for element substitution. + * @author Daniel Bell (dbell@netbeans.org) + */ +public class AbstractElementTest extends AbstractTestCase { + + static final String COMPLETION_DOCUMENT = "resources/AbstractElementCompletion.xml"; + static final String PARENT_SCHEMA = "resources/AbstractElementParent.xsd"; + static final String CHILD_SCHEMA_ONE = "resources/AbstractElementChildOne.xsd"; + static final String CHILD_SCHEMA_TWO = "resources/AbstractElementChildTwo.xsd"; + + public AbstractElementTest(String testName) { + super(testName); + } + + public static Test suite() { + TestSuite suite = new TestSuite(); + suite.addTest(new AbstractElementTest("shouldNotSuggestAbstractElement")); + suite.addTest(new AbstractElementTest("shouldExpandSubstitutionGroup")); + return suite; + } + + @Override + public void setUp() throws Exception { + setupCompletion(COMPLETION_DOCUMENT); + } + + /** + * Elements with {@code abstract="true"} should not be suggested + */ + public void shouldNotSuggestAbstractElement() { + List items = query(468); + assertThat(items, not(containsSuggestions("child"))); + } + + /** + * All available elements that can be substituted for elements in the + * completion context should be presented as completion options + */ + public void shouldExpandSubstitutionGroup() { + List items = query(468); + assertThat(items, containsOnlySuggestions("c1:child-one", "c2:child-two")); + } +} diff --git a/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/AbstractTestCase.java b/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/AbstractTestCase.java --- a/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/AbstractTestCase.java +++ b/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/AbstractTestCase.java @@ -43,8 +43,13 @@ */ package org.netbeans.modules.xml.schema.completion; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import junit.framework.*; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.junit.internal.matchers.TypeSafeMatcher; import org.netbeans.api.lexer.Language; import org.netbeans.api.xml.lexer.XMLTokenId; import org.netbeans.editor.BaseDocument; @@ -75,14 +80,30 @@ protected void tearDown() throws Exception { } - protected void setupCompletion(String path, StringBuffer buffer) throws Exception { + /** + * Set up the test for a particular XML document + * @param path the XML document + * @see #setupCompletion(java.lang.String, java.lang.StringBuffer) + */ + protected void setupCompletion(String path) throws Exception { + setupCompletion(path, null); + } + + + /** + * Set up the test for a particular XML document + * @param path the XML document + * @param content the content to insert into the document + * @see #setupCompletion(java.lang.String) + */ + protected void setupCompletion(String path, StringBuffer content) throws Exception { this.instanceResourcePath = path; this.instanceFileObject = Util.getResourceAsFileObject(path); this.instanceDocument = Util.getResourceAsDocument(path); this.support = ((XMLSyntaxSupport)instanceDocument.getSyntaxSupport()); - if(buffer != null) { + if(content != null) { instanceDocument.remove(0, instanceDocument.getLength()); - instanceDocument.insertString(0, buffer.toString(), null); + instanceDocument.insertString(0, content.toString(), null); } instanceDocument.putProperty(Language.class, XMLTokenId.language()); } @@ -90,15 +111,17 @@ /** * Queries and returns a list of completion items. * Each unit test is supposed to evaluate this result. + * @param caretOffset the caret offset at which code completion is invoked + * @return the code completion results */ - protected List query(int caretOffset) throws Exception { + protected List query(int caretOffset) { assert(instanceFileObject != null && instanceDocument != null); CompletionQuery instance = new CompletionQuery(instanceFileObject); return instance.getCompletionItems(instanceDocument, caretOffset); } protected void assertResult(List result, - String[] expectedResult) { + String... expectedResult) { if(result == null && expectedResult == null) { assert(true); return; @@ -136,6 +159,14 @@ } } + protected Matcher> containsSuggestions(String... suggestions) { + return new SuggestionsContaining(false, suggestions); + } + + protected Matcher> containsOnlySuggestions(String... suggestions) { + return new SuggestionsContaining(true, suggestions); + } + BaseDocument getDocument() { return instanceDocument; } @@ -153,4 +184,40 @@ context.initContext(); return context; } + + private class SuggestionsContaining extends TypeSafeMatcher> { + + private final List expectedSuggestionStrings; + private final boolean expectingExactMatch; + public SuggestionsContaining(boolean expectingExactMatch, String... suggestions) { + this.expectedSuggestionStrings = Arrays.asList(suggestions); + this.expectingExactMatch = expectingExactMatch; + } + + @Override + public boolean matchesSafely(List actual) { + if(actual == null) { + return false; + } + if(expectingExactMatch && actual.size() != expectedSuggestionStrings.size()) { + return false; + } + List actualSuggestionStrings = new ArrayList(actual.size()); + for (CompletionResultItem result : actual) { + actualSuggestionStrings.add(result.getItemText()); + } + return actualSuggestionStrings.containsAll(expectedSuggestionStrings); + } + + @Override + public void describeTo(Description d) { + d.appendText("completion results "); + if(expectingExactMatch) { + d.appendText("exactly matching "); + } else { + d.appendText("containing "); + } + d.appendValue(expectedSuggestionStrings); + } + } } diff --git a/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/resources/AbstractElementChildOne.xsd b/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/resources/AbstractElementChildOne.xsd new file mode 100644 --- /dev/null +++ b/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/resources/AbstractElementChildOne.xsd @@ -0,0 +1,21 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/resources/AbstractElementChildTwo.xsd b/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/resources/AbstractElementChildTwo.xsd new file mode 100644 --- /dev/null +++ b/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/resources/AbstractElementChildTwo.xsd @@ -0,0 +1,21 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/resources/AbstractElementCompletion.xml b/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/resources/AbstractElementCompletion.xml new file mode 100644 --- /dev/null +++ b/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/resources/AbstractElementCompletion.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/resources/AbstractElementParent.xsd b/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/resources/AbstractElementParent.xsd new file mode 100644 --- /dev/null +++ b/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/resources/AbstractElementParent.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + diff --git a/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/resources/Include.xml b/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/resources/Include.xml --- a/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/resources/Include.xml +++ b/xml.schema.completion/test/unit/src/org/netbeans/modules/xml/schema/completion/resources/Include.xml @@ -1,5 +1,5 @@ - < diff --git a/xml.schema.model/apichanges.xml b/xml.schema.model/apichanges.xml new file mode 100644 --- /dev/null +++ b/xml.schema.model/apichanges.xml @@ -0,0 +1,146 @@ + + + + + + + + + + Schema Model API + Schema Model Visitors + + + + + + Added FindSubstitutionsVisitor to find all valid substitutions for global elements + + + + + +

+ Added FindSubstitutionsVisitor: a schema model visitor that + finds all valid substitutions (global elements) for a given + substitution group head. Substitutions are found from all + schemas referenced by the target SchemaModel. +

+
+ + +
+
+ + + + + Change History for the CHANGEME API + + + + +

Introduction

+

This document lists changes made to the + CHANGEME API. +

+ +
+ +
+

@FOOTER@

+ +
+
diff --git a/xml.schema.model/nbproject/project.properties b/xml.schema.model/nbproject/project.properties --- a/xml.schema.model/nbproject/project.properties +++ b/xml.schema.model/nbproject/project.properties @@ -45,4 +45,5 @@ is.autoload=true javac.source=1.6 javadoc.arch=${basedir}/arch.xml -spec.version.base=1.16.0 +javadoc.apichanges=${basedir}/apichanges.xml +spec.version.base=1.17.0 diff --git a/xml.schema.model/src/org/netbeans/modules/xml/schema/model/GlobalElement.java b/xml.schema.model/src/org/netbeans/modules/xml/schema/model/GlobalElement.java --- a/xml.schema.model/src/org/netbeans/modules/xml/schema/model/GlobalElement.java +++ b/xml.schema.model/src/org/netbeans/modules/xml/schema/model/GlobalElement.java @@ -87,6 +87,11 @@ Set getFinalDefault(); Set getFinalEffective(); + /** + * The substitution group to which this element belongs + * @return the substitution group to which this element belongs, or null if + * the element does not belong to a substitution group + */ NamedComponentReference getSubstitutionGroup(); void setSubstitutionGroup(NamedComponentReference element); diff --git a/xml.schema.model/src/org/netbeans/modules/xml/schema/model/SchemaModel.java b/xml.schema.model/src/org/netbeans/modules/xml/schema/model/SchemaModel.java --- a/xml.schema.model/src/org/netbeans/modules/xml/schema/model/SchemaModel.java +++ b/xml.schema.model/src/org/netbeans/modules/xml/schema/model/SchemaModel.java @@ -110,7 +110,7 @@ * @param type type of the component. */ T resolve(String namespace, String localName, Class type); - + /** * Returns true for schemas that are embedded inside other artifacts such as WSDLs and BPELs. * False in all other cases. diff --git a/xml.schema.model/src/org/netbeans/modules/xml/schema/model/impl/SchemaModelImpl.java b/xml.schema.model/src/org/netbeans/modules/xml/schema/model/impl/SchemaModelImpl.java --- a/xml.schema.model/src/org/netbeans/modules/xml/schema/model/impl/SchemaModelImpl.java +++ b/xml.schema.model/src/org/netbeans/modules/xml/schema/model/impl/SchemaModelImpl.java @@ -44,25 +44,15 @@ package org.netbeans.modules.xml.schema.model.impl; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; +import java.util.*; import java.util.List; import java.util.Map; import java.util.Set; import javax.xml.XMLConstants; import javax.xml.namespace.QName; -import org.netbeans.modules.xml.schema.model.Import; -import org.netbeans.modules.xml.schema.model.Include; -import org.netbeans.modules.xml.schema.model.Redefine; +import org.netbeans.modules.xml.schema.model.*; import org.netbeans.modules.xml.xam.locator.CatalogModelException; import org.netbeans.modules.xml.xam.ModelSource; -import org.netbeans.modules.xml.schema.model.Schema; -import org.netbeans.modules.xml.schema.model.SchemaComponent; -import org.netbeans.modules.xml.schema.model.SchemaComponentFactory; -import org.netbeans.modules.xml.schema.model.SchemaModel; -import org.netbeans.modules.xml.schema.model.SchemaModelFactory; -import org.netbeans.modules.xml.schema.model.SchemaModelReference; import org.netbeans.modules.xml.schema.model.impl.xdm.SyncUpdateVisitor; import org.netbeans.modules.xml.xam.ComponentUpdater; import org.netbeans.modules.xml.xam.Model; @@ -152,7 +142,7 @@ public SchemaComponent getRootComponent() { return mSchema; } - + @Override public T resolve(String namespace, String localName, Class type) diff --git a/xml.schema.model/src/org/netbeans/modules/xml/schema/model/visitor/FindSubstitutionsVisitor.java b/xml.schema.model/src/org/netbeans/modules/xml/schema/model/visitor/FindSubstitutionsVisitor.java new file mode 100644 --- /dev/null +++ b/xml.schema.model/src/org/netbeans/modules/xml/schema/model/visitor/FindSubstitutionsVisitor.java @@ -0,0 +1,146 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2011 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU General + * Public License Version 2 only ("GPL") or the Common Development and + * Distribution License("CDDL") (collectively, the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy of the + * License at http://www.netbeans.org/cddl-gplv2.html or + * nbbuild/licenses/CDDL-GPL-2-CP. See the License for the specific language + * governing permissions and limitations under the License. When distributing + * the software, include this License Header Notice in each file and include the + * License file at nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided by Oracle + * in the GPL Version 2 section of the License file that accompanied this code. + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL or only + * the GPL Version 2, indicate your decision by adding "[Contributor] elects to + * include this software in this distribution under the [CDDL or GPL Version 2] + * license." If you do not indicate a single choice of license, a recipient has + * the option to distribute your version of this file under either the CDDL, the + * GPL Version 2 or to extend the choice of license to its licensees as provided + * above. However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is made + * subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2011 Sun Microsystems, Inc. + */ +package org.netbeans.modules.xml.schema.model.visitor; + +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.netbeans.modules.xml.schema.model.*; +import org.netbeans.modules.xml.xam.dom.NamedComponentReference; +import org.netbeans.modules.xml.xam.locator.CatalogModelException; + +/** + * Resolves substitutions for {@link GlobalElement}s which are referenced by + * other {@link GlobalElement}s as substitution groups. + * + * @author Daniel Bell (dbell@netbeans.org) + * @see #resolveSubstitutions + * @since org.netbeans.modules.xml.schema.model/1 1.17 + */ +public final class FindSubstitutionsVisitor extends DeepSchemaVisitor { + + /** + * Find all substitutions for a {@link GlobalElement} that is referenced by + * other {@link GlobalElement}s as a substitution group. All referenced + * schemas (via <import> / <include> /<redefine>) are + * searched for substitution possibilities. + * @param model the model to search for substitutions for the specified + * substitution group head + * @param namespaceUri the namespace URI of the substitution group head + * @param localName local name of the substitution group head (must resolve + * to a global element) + * @return all possible substitutions for the specified type + */ + public static Set resolveSubstitutions(SchemaModel model, String namespaceUri, String localName) { + GlobalElement element = model.resolve(namespaceUri, localName, GlobalElement.class); + if(element == null) { + return Collections.emptySet(); + } else { + return resolveSubstitutions(model, element); + } + } + + /** + * Find all substitutions for a {@link GlobalElement} that is referenced by + * other {@link GlobalElement}s as a substitution group. All referenced + * schemas (via <import> / <include> /<redefine>) are + * searched for substitution possibilities. + * @param model the model to search for substitutions for the specified + * substitution group head + * @param substitutionGroupHead the element for which to search for + * substitutable elements + * @return all elements reachable from the provided model that specify + * the provided element as their {@code substitutionGroup} + */ + public static Set resolveSubstitutions(SchemaModel model, GlobalElement substitutionGroupHead) { + Collection schemasToSearch = new LinkedList(); + + //Look in current schema + Schema startSchema = model.getSchema(); + schemasToSearch.add(startSchema); + + //Look in all referenced schemas + Collection referencedSchemas = startSchema.getSchemaReferences(); + for (SchemaModelReference reference : referencedSchemas) { + try { + SchemaModel referencedModel = reference.resolveReferencedModel(); + schemasToSearch.add(referencedModel.getSchema()); + } catch (CatalogModelException ex) { + Logger.getLogger(FindSubstitutionsVisitor.class.getName()).log(Level.FINE, "Could not resolve schema reference", ex); + } + } + FindSubstitutionsVisitor visitor = new FindSubstitutionsVisitor(substitutionGroupHead); + for (Schema schema : schemasToSearch) { + schema.accept(visitor); + } + return Collections.unmodifiableSet(visitor.substitutions); + } + + private final Set substitutions = new LinkedHashSet(); + private final GlobalElement substitutionGroupBase; + + public FindSubstitutionsVisitor(GlobalElement substitutionGroupBase) { + this.substitutionGroupBase = substitutionGroupBase; + } + + @Override + public void visit(ElementReference element) { + NamedComponentReference reference = element.getRef(); + if (!reference.isBroken()) { + addIfInSubstitutionGroup(reference.get()); + } + super.visit(element); + } + + @Override + public void visit(GlobalElement element) { + addIfInSubstitutionGroup(element); + super.visit(element); + } + + private void addIfInSubstitutionGroup(GlobalElement potentialSubstitute) { + NamedComponentReference substitutionGroupReference = potentialSubstitute.getSubstitutionGroup(); + boolean hasResolvableSubGroup = !(substitutionGroupReference == null || substitutionGroupReference.isBroken()); + if (hasResolvableSubGroup) { + GlobalElement referencedSubstitutionGroup = substitutionGroupReference.get(); + if (substitutionGroupBase.equals(referencedSubstitutionGroup)) { + substitutions.add(potentialSubstitute); + } + } + } +} diff --git a/xml.schema.model/src/org/netbeans/modules/xml/schema/model/visitor/PreviewImpl.java b/xml.schema.model/src/org/netbeans/modules/xml/schema/model/visitor/PreviewImpl.java --- a/xml.schema.model/src/org/netbeans/modules/xml/schema/model/visitor/PreviewImpl.java +++ b/xml.schema.model/src/org/netbeans/modules/xml/schema/model/visitor/PreviewImpl.java @@ -68,6 +68,14 @@ /** * Returns a collection of schema components, all of which, * reference the same global schema component. + * @return a Map of usages to their path from their respective schema's root.
+ * Example:
+ *
+     * { 
+     *    myElement : [mySchema > myElement], 
+     *    myOtherElement : [myOtherSchema > myType > myOtherElement] 
+     * }
+     * 
*/ public Map> getUsages() { return usages; diff --git a/xml.schema.model/test/unit/src/org/netbeans/modules/xml/schema/model/TestCatalogModel.java b/xml.schema.model/test/unit/src/org/netbeans/modules/xml/schema/model/TestCatalogModel.java --- a/xml.schema.model/test/unit/src/org/netbeans/modules/xml/schema/model/TestCatalogModel.java +++ b/xml.schema.model/test/unit/src/org/netbeans/modules/xml/schema/model/TestCatalogModel.java @@ -41,16 +41,6 @@ * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. */ - -/* - * TestCatalogModel.java - * - * Created on April 2, 2006, 10:41 AM - * - * To change this template, choose Tools | Template Manager - * and open the template in the editor. - */ - package org.netbeans.modules.xml.schema.model; import java.io.File; @@ -60,13 +50,14 @@ import java.util.HashMap; import java.util.Map; import javax.swing.text.Document; +import org.junit.rules.ExternalResource; +import org.junit.rules.TestRule; import org.netbeans.editor.BaseDocument; -import org.netbeans.editor.BaseKit; import org.netbeans.modules.xml.retriever.catalog.impl.CatalogFileWrapperDOMImpl; import org.netbeans.modules.xml.retriever.catalog.impl.CatalogWriteModelImpl; +import org.netbeans.modules.xml.xam.ModelSource; import org.netbeans.modules.xml.xam.locator.CatalogModel; import org.netbeans.modules.xml.xam.locator.CatalogModelException; -import org.netbeans.modules.xml.xam.ModelSource; import org.openide.cookies.SaveCookie; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; @@ -79,7 +70,6 @@ * * @author girix */ - public class TestCatalogModel extends CatalogWriteModelImpl{ private TestCatalogModel(File file) throws IOException{ super(file); @@ -234,5 +224,24 @@ public void clearDocumentPool() { fileToDocumentMap = null; } + + /** + * A JUnit {@link TestRule} that stops tests from interfering with one + * another. JUnit will automatically set up/clean up the catalog when this + * rule is used.
+ * Usage:
+ * {@code @Rule public final TestRule catalogMaintainer = TestCatalogModel.maintainer()} + * @return the TestRule + */ + public static TestRule maintainer() { + return new ExternalResource() { + + @Override + protected void after() { + getDefault().clearDocumentPool(); + } + + }; + } } diff --git a/xml.schema.model/test/unit/src/org/netbeans/modules/xml/schema/model/resources/SubstitutionGroupChildOne.xsd b/xml.schema.model/test/unit/src/org/netbeans/modules/xml/schema/model/resources/SubstitutionGroupChildOne.xsd new file mode 100644 --- /dev/null +++ b/xml.schema.model/test/unit/src/org/netbeans/modules/xml/schema/model/resources/SubstitutionGroupChildOne.xsd @@ -0,0 +1,21 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/xml.schema.model/test/unit/src/org/netbeans/modules/xml/schema/model/resources/SubstitutionGroupChildTwo.xsd b/xml.schema.model/test/unit/src/org/netbeans/modules/xml/schema/model/resources/SubstitutionGroupChildTwo.xsd new file mode 100644 --- /dev/null +++ b/xml.schema.model/test/unit/src/org/netbeans/modules/xml/schema/model/resources/SubstitutionGroupChildTwo.xsd @@ -0,0 +1,21 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/xml.schema.model/test/unit/src/org/netbeans/modules/xml/schema/model/resources/SubstitutionGroupParent.xsd b/xml.schema.model/test/unit/src/org/netbeans/modules/xml/schema/model/resources/SubstitutionGroupParent.xsd new file mode 100644 --- /dev/null +++ b/xml.schema.model/test/unit/src/org/netbeans/modules/xml/schema/model/resources/SubstitutionGroupParent.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + diff --git a/xml.schema.model/test/unit/src/org/netbeans/modules/xml/schema/model/visitor/FindSubstitutionsVisitorTest.java b/xml.schema.model/test/unit/src/org/netbeans/modules/xml/schema/model/visitor/FindSubstitutionsVisitorTest.java new file mode 100644 --- /dev/null +++ b/xml.schema.model/test/unit/src/org/netbeans/modules/xml/schema/model/visitor/FindSubstitutionsVisitorTest.java @@ -0,0 +1,108 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2011 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU General + * Public License Version 2 only ("GPL") or the Common Development and + * Distribution License("CDDL") (collectively, the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy of the + * License at http://www.netbeans.org/cddl-gplv2.html or + * nbbuild/licenses/CDDL-GPL-2-CP. See the License for the specific language + * governing permissions and limitations under the License. When distributing + * the software, include this License Header Notice in each file and include the + * License file at nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided by Oracle + * in the GPL Version 2 section of the License file that accompanied this code. + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL or only + * the GPL Version 2, indicate your decision by adding "[Contributor] elects to + * include this software in this distribution under the [CDDL or GPL Version 2] + * license." If you do not indicate a single choice of license, a recipient has + * the option to distribute your version of this file under either the CDDL, the + * GPL Version 2 or to extend the choice of license to its licensees as provided + * above. However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is made + * subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2011 Sun Microsystems, Inc. + */ +package org.netbeans.modules.xml.schema.model.visitor; + +import java.util.Set; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import static org.junit.matchers.JUnitMatchers.hasItem; +import org.junit.rules.TestRule; +import org.netbeans.modules.xml.schema.model.GlobalElement; +import org.netbeans.modules.xml.schema.model.TestCatalogModel; +import org.netbeans.modules.xml.schema.model.Util; +import org.netbeans.modules.xml.schema.model.impl.SchemaModelImpl; + +/** + * Tests for {@link FindSubstitutionsVisitor} + * @author Daniel Bell (dbell@netbeans.org) + */ +public class FindSubstitutionsVisitorTest { + public static final String PARENT_NS_URI = "urn:parent"; + public static final String SUBSTITUTION_GROUP_HEAD = "child"; + private static final String PARENT_SCHEMA = "resources/SubstitutionGroupParent.xsd"; + private static final String CHILD_SCHEMA_ONE = "resources/SubstitutionGroupChildOne.xsd"; + private static final String CHILD_SCHEMA_TWO = "resources/SubstitutionGroupChildTwo.xsd"; + private static final String SUBSTITUTION_ELEMENT_ONE = "child-one"; + private static final String SUBSTITUTION_ELEMENT_TWO = "child-two"; + + @Rule + public final TestRule catalogMaintainer = TestCatalogModel.maintainer(); + + private SchemaModelImpl parentModel; + private SchemaModelImpl childModelOne; + private SchemaModelImpl childModelTwo; + + @Before + public void setUp() throws Exception { + childModelOne = load(CHILD_SCHEMA_ONE); + childModelTwo = load(CHILD_SCHEMA_TWO); + parentModel = load(PARENT_SCHEMA); + } + + /** + * Each schema imports a substitution group base, and defines a global + * element that is part of this substitution group. + * FindSubstitutionsVisitor should resolve this substitution. + */ + @Test + public void shouldResolveSubstitutionsFromLinkedSchemas() { + GlobalElement substitutionGroupHead = getCachedElement(parentModel, SUBSTITUTION_GROUP_HEAD); + GlobalElement expectedSubstitutionOne = getCachedElement(childModelOne, SUBSTITUTION_ELEMENT_ONE); + GlobalElement expectedSubstitutionTwo = getCachedElement(childModelTwo, SUBSTITUTION_ELEMENT_TWO); + + Set possibleSubstitutionsOne = FindSubstitutionsVisitor.resolveSubstitutions(childModelOne, substitutionGroupHead); + Set possibleSubstitutionsTwo = FindSubstitutionsVisitor.resolveSubstitutions(childModelTwo, substitutionGroupHead); + + assertThat(possibleSubstitutionsOne.size(), is(1)); + assertThat(possibleSubstitutionsOne, hasItem(expectedSubstitutionOne)); + + assertThat(possibleSubstitutionsTwo.size(), is(1)); + assertThat(possibleSubstitutionsTwo, hasItem(expectedSubstitutionTwo)); //Should used cached substitution group head + } + + private static GlobalElement getCachedElement(SchemaModelImpl model, String localName) { + return model.getGlobalComponentsIndexSupport().findByNameAndType(localName, GlobalElement.class); + } + + private static SchemaModelImpl load(String schemaPath) throws Exception { + return Util.toSchemaModelImpl(Util.loadSchemaModel2(schemaPath)); + } +} diff --git a/xml.xam/src/org/netbeans/modules/xml/xam/dom/AbstractDocumentComponent.java b/xml.xam/src/org/netbeans/modules/xml/xam/dom/AbstractDocumentComponent.java --- a/xml.xam/src/org/netbeans/modules/xml/xam/dom/AbstractDocumentComponent.java +++ b/xml.xam/src/org/netbeans/modules/xml/xam/dom/AbstractDocumentComponent.java @@ -821,6 +821,11 @@ CatalogModel nr = (CatalogModel) getModel().getModelSource().getLookup().lookup(CatalogModel.class); + if(nr == null) { + String error = String.format("Cannot resolve file [hint = %s, backup = %s], because no CatalogModel exists in lookup", hint, backup); + throw new CatalogModelException(error); + } + // try hint ModelSource ms = resolveModelSource(hint, getModel().getModelSource(), nr);