From 10255d09e84001787647b200c04c293da2d7731a Mon Sep 17 00:00:00 2001 From: irengrig Date: Mon, 4 Apr 2016 12:28:42 +0200 Subject: [PATCH] WEB-20415 JSON Schema: support references to definitions --- .../jsonSchema/JsonSchemaFileType.java | 69 ++++++++++++ .../jsonSchema/JsonSchemaFileTypeFactory.java | 30 ++++++ .../JsonSchemaProjectSelfProviderFactory.java | 7 +- .../schema/JsonSchemaBaseReference.java | 76 +++++++++++++ .../extension/schema/JsonSchemaFileIndex.java | 101 ++++++++++++++++++ .../JsonSchemaRefReferenceProvider.java | 93 ++++++++++++++++ .../JsonSchemaReferenceContributor.java | 73 +++++++++++++ .../jsonSchema/ide/JsonSchemaService.java | 7 +- .../impl/JsonSchemaExportedDefinitions.java | 25 +++-- .../jsonSchema/impl/JsonSchemaObject.java | 2 +- .../jsonSchema/impl/JsonSchemaReader.java | 2 +- .../jsonSchema/impl/JsonSchemaServiceEx.java | 28 +++++ .../impl/JsonSchemaServiceImpl.java | 19 +++- .../JsonSchemaCrossReferencesTest.java | 74 +++++++++++++ .../jsonSchema/JsonSchemaTestProvider.java | 5 +- .../schemaFile/JsonSchemaFileResolveTest.java | 94 ++++++++++++++++ ...sonSchemaMappingsProjectConfiguration.java | 35 ++++++ .../schemaFile/resolve/localRefSchema.json | 12 +++ .../schemaFile/resolve/referencingSchema.json | 7 ++ .../src/META-INF/JsonPlugin.xml | 4 + 20 files changed, 743 insertions(+), 20 deletions(-) create mode 100644 json/src/com/jetbrains/jsonSchema/JsonSchemaFileType.java create mode 100644 json/src/com/jetbrains/jsonSchema/JsonSchemaFileTypeFactory.java create mode 100644 json/src/com/jetbrains/jsonSchema/extension/schema/JsonSchemaBaseReference.java create mode 100644 json/src/com/jetbrains/jsonSchema/extension/schema/JsonSchemaFileIndex.java create mode 100644 json/src/com/jetbrains/jsonSchema/extension/schema/JsonSchemaRefReferenceProvider.java create mode 100644 json/src/com/jetbrains/jsonSchema/extension/schema/JsonSchemaReferenceContributor.java create mode 100644 json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceEx.java create mode 100644 json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaFileResolveTest.java create mode 100644 json/tests/test/com/jetbrains/jsonSchema/schemaFile/TestJsonSchemaMappingsProjectConfiguration.java create mode 100644 json/tests/testData/jsonSchema/schemaFile/resolve/localRefSchema.json create mode 100644 json/tests/testData/jsonSchema/schemaFile/resolve/referencingSchema.json diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaFileType.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaFileType.java new file mode 100644 index 000000000000..c337929499fe --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaFileType.java @@ -0,0 +1,69 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed 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 com.jetbrains.jsonSchema; + +import com.intellij.icons.AllIcons; +import com.intellij.json.JsonLanguage; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.fileTypes.ex.FileTypeIdentifiableByVirtualFile; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectUtil; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +/** + * @author Irina.Chernushina on 4/1/2016. + */ +public class JsonSchemaFileType extends LanguageFileType implements FileTypeIdentifiableByVirtualFile { + public static JsonSchemaFileType INSTANCE = new JsonSchemaFileType(); + + public JsonSchemaFileType() { + super(JsonLanguage.INSTANCE); + } + + @NotNull + @Override + public String getName() { + return "JSON Schema"; + } + + @NotNull + @Override + public String getDescription() { + return "JSON Schema file"; + } + + @NotNull + @Override + public String getDefaultExtension() { + return "json"; + } + + @Nullable + @Override + public Icon getIcon() {//todo change icon + return AllIcons.FileTypes.Json; + } + + @Override + public boolean isMyFileType(@NotNull VirtualFile file) { + final Project project = ProjectUtil.guessProjectForFile(file); + return project != null && JsonSchemaMappingsProjectConfiguration.getInstance(project).isRegisteredSchemaFile(file); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaFileTypeFactory.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaFileTypeFactory.java new file mode 100644 index 000000000000..d374ed4baa2a --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaFileTypeFactory.java @@ -0,0 +1,30 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed 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 com.jetbrains.jsonSchema; + +import com.intellij.openapi.fileTypes.FileTypeConsumer; +import com.intellij.openapi.fileTypes.FileTypeFactory; +import org.jetbrains.annotations.NotNull; + +/** + * @author Irina.Chernushina on 4/1/2016. + */ +public class JsonSchemaFileTypeFactory extends FileTypeFactory { + @Override + public void createFileTypes(@NotNull FileTypeConsumer consumer) { + consumer.consume(JsonSchemaFileType.INSTANCE); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProjectSelfProviderFactory.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProjectSelfProviderFactory.java index 9a954a60e277..46477e7c7353 100644 --- a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProjectSelfProviderFactory.java +++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProjectSelfProviderFactory.java @@ -15,13 +15,13 @@ */ package com.jetbrains.jsonSchema.extension; -import com.intellij.json.JsonFileType; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ResourceUtil; +import com.jetbrains.jsonSchema.JsonSchemaFileType; import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -38,6 +38,7 @@ import java.util.List; */ public class JsonSchemaProjectSelfProviderFactory { private static final Logger LOG = Logger.getInstance("#com.jetbrains.jsonSchema.extension.JsonSchemaProjectSelfProviderFactory"); + public static final String SCHEMA_JSON_FILE_NAME = "schema.json"; private final List> myProviders; public static JsonSchemaProjectSelfProviderFactory getInstance(final Project project) { @@ -62,7 +63,7 @@ public class JsonSchemaProjectSelfProviderFactory { @Override public boolean isAvailable(@NotNull VirtualFile file) { - if (myProject == null || !JsonFileType.INSTANCE.equals(file.getFileType())) return false; + if (myProject == null || !JsonSchemaFileType.INSTANCE.equals(file.getFileType())) return false; return JsonSchemaMappingsProjectConfiguration.getInstance(myProject).isRegisteredSchemaFile(file); } @@ -76,7 +77,7 @@ public class JsonSchemaProjectSelfProviderFactory { @NotNull @Override public String getName() { - return "schema.json"; + return SCHEMA_JSON_FILE_NAME; } @NotNull diff --git a/json/src/com/jetbrains/jsonSchema/extension/schema/JsonSchemaBaseReference.java b/json/src/com/jetbrains/jsonSchema/extension/schema/JsonSchemaBaseReference.java new file mode 100644 index 000000000000..73c093a4be48 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/schema/JsonSchemaBaseReference.java @@ -0,0 +1,76 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed 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 com.jetbrains.jsonSchema.extension.schema; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import com.intellij.psi.PsiReferenceBase; +import com.intellij.psi.impl.source.resolve.ResolveCache; +import com.intellij.util.ArrayUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Irina.Chernushina on 3/31/2016. + */ +public abstract class JsonSchemaBaseReference extends PsiReferenceBase { + public JsonSchemaBaseReference(T element, TextRange textRange) { + super(element, textRange, true); + } + + @Nullable + @Override + public PsiElement resolve() { + return ResolveCache.getInstance(getElement().getProject()).resolveWithCaching(this, MyResolver.INSTANCE, false, false); + } + + @Nullable + public abstract PsiElement resolveInner(); + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JsonSchemaBaseReference that = (JsonSchemaBaseReference)o; + + if (!myElement.equals(that.myElement)) return false; + + return true; + } + + @Override + public int hashCode() { + return myElement.hashCode(); + } + + @NotNull + @Override + public Object[] getVariants() { + return ArrayUtil.EMPTY_OBJECT_ARRAY; + } + + private static class MyResolver implements ResolveCache.Resolver { + private static final MyResolver INSTANCE = new MyResolver(); + + @Nullable + public PsiElement resolve(@NotNull PsiReference ref, boolean incompleteCode) { + return ((JsonSchemaBaseReference)ref).resolveInner(); + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/schema/JsonSchemaFileIndex.java b/json/src/com/jetbrains/jsonSchema/extension/schema/JsonSchemaFileIndex.java new file mode 100644 index 000000000000..bab4ee9b59d9 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/schema/JsonSchemaFileIndex.java @@ -0,0 +1,101 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed 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 com.jetbrains.jsonSchema.extension.schema; + +import com.intellij.json.psi.JsonFile; +import com.intellij.json.psi.JsonProperty; +import com.intellij.json.psi.impl.JsonRecursiveElementVisitor; +import com.intellij.psi.PsiFile; +import com.intellij.util.indexing.*; +import com.intellij.util.io.DataExternalizer; +import com.intellij.util.io.EnumeratorStringDescriptor; +import com.intellij.util.io.IntInlineKeyDescriptor; +import com.intellij.util.io.KeyDescriptor; +import com.jetbrains.jsonSchema.JsonSchemaFileType; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Irina.Chernushina on 4/1/2016. + */ +public class JsonSchemaFileIndex extends FileBasedIndexExtension { + public static final ID PROPERTIES_INDEX = ID.create("json.schema.properties.index"); + public static final int VERSION = 1; + private IntInlineKeyDescriptor myKeyDescriptor = new IntInlineKeyDescriptor(); + + @NotNull + @Override + public ID getName() { + return PROPERTIES_INDEX; + } + + @NotNull + @Override + public DataIndexer getIndexer() { + return new DataIndexer() { + @NotNull + @Override + public Map map(@NotNull FileContent inputData) { + final Map map = new HashMap<>(); + final PsiFile file = inputData.getPsiFile(); + if (file instanceof JsonFile) { + file.accept(new JsonRecursiveElementVisitor() { + private String myPrefix = ""; + @Override + public void visitProperty(@NotNull JsonProperty property) { + String wasPrefix = myPrefix; + myPrefix = myPrefix + "/" + property.getName(); + map.put(myPrefix, property.getTextRange().getStartOffset()); + super.visitProperty(property); + myPrefix = wasPrefix; + } + }); + } + return map; + } + }; + } + + @NotNull + @Override + public KeyDescriptor getKeyDescriptor() { + return EnumeratorStringDescriptor.INSTANCE; + } + + @NotNull + @Override + public DataExternalizer getValueExternalizer() { + return myKeyDescriptor; + } + + @Override + public int getVersion() { + return VERSION; + } + + @NotNull + @Override + public FileBasedIndex.InputFilter getInputFilter() { + return new DefaultFileTypeSpecificInputFilter(JsonSchemaFileType.INSTANCE); + } + + @Override + public boolean dependsOnFileContent() { + return true; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/schema/JsonSchemaRefReferenceProvider.java b/json/src/com/jetbrains/jsonSchema/extension/schema/JsonSchemaRefReferenceProvider.java new file mode 100644 index 000000000000..d0b10c898dc3 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/schema/JsonSchemaRefReferenceProvider.java @@ -0,0 +1,93 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed 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 com.jetbrains.jsonSchema.extension.schema; + +import com.intellij.json.psi.JsonValue; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.*; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.util.ProcessingContext; +import com.intellij.util.indexing.FileBasedIndex; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Irina.Chernushina on 3/31/2016. + */ +public class JsonSchemaRefReferenceProvider extends PsiReferenceProvider { + @NotNull + @Override + public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) { + return new PsiReference[] {new JsonSchemaRefReference((JsonValue)element)}; + } + + private static class JsonSchemaRefReference extends JsonSchemaBaseReference { + public JsonSchemaRefReference(JsonValue element) { + super(element, ElementManipulators.getValueTextRange(element)); + } + + @NotNull + @Override + public String getCanonicalText() { + return StringUtil.unquoteString(super.getCanonicalText()); + } + + @Nullable + @Override + public PsiElement resolveInner() { + final FileBasedIndex index = FileBasedIndex.getInstance(); + final String text = getCanonicalText(); + String id = null; + String ref = text.substring(1); + if (!text.startsWith("#")) { + final int idx = text.indexOf("#"); + if (idx <= 0) return null; + id = text.substring(0, idx); + ref = text.substring(idx + 1); + } + + final Ref> reference = new Ref<>(); + final GlobalSearchScope filter = id == null ? GlobalSearchScope.fileScope(getElement().getContainingFile()) : + GlobalSearchScope.allScope(getElement().getProject()); + String finalId = id; + index.processValues(JsonSchemaFileIndex.PROPERTIES_INDEX, ref, null, new FileBasedIndex.ValueProcessor() { + @Override + public boolean process(VirtualFile file, Integer value) { + if (finalId != null) { + if (!JsonSchemaService.Impl.getEx(getElement().getProject()).checkFileForId(finalId, file)) { + return true; + } + } + reference.set(Pair.create(file, value)); + return false; + } + }, filter); + + if (!reference.isNull()) { + final Pair pair = reference.get(); + final PsiFile file = getElement().getManager().findFile(pair.getFirst()); + if (file != null) { + return file.findElementAt(pair.getSecond()); + } + } + return null; + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/schema/JsonSchemaReferenceContributor.java b/json/src/com/jetbrains/jsonSchema/extension/schema/JsonSchemaReferenceContributor.java new file mode 100644 index 000000000000..0a7a3e321afe --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/schema/JsonSchemaReferenceContributor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed 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 com.jetbrains.jsonSchema.extension.schema; + +import com.intellij.json.psi.JsonProperty; +import com.intellij.json.psi.JsonValue; +import com.intellij.openapi.project.Project; +import com.intellij.patterns.PlatformPatterns; +import com.intellij.patterns.PsiElementPattern; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiReferenceContributor; +import com.intellij.psi.PsiReferenceRegistrar; +import com.intellij.psi.filters.ElementFilter; +import com.intellij.psi.filters.position.FilterPattern; +import com.jetbrains.jsonSchema.JsonSchemaFileType; +import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Irina.Chernushina on 3/31/2016. + */ +public class JsonSchemaReferenceContributor extends PsiReferenceContributor { + private static final PsiElementPattern.Capture REF_PATTERN = createPropertyValuePattern("$ref"); + + @Override + public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) { + registrar.registerReferenceProvider(REF_PATTERN, new JsonSchemaRefReferenceProvider()); + } + + private static PsiElementPattern.Capture createPropertyValuePattern(@NotNull final String propertyName) { + return PlatformPatterns.psiElement(JsonValue.class).and(new FilterPattern(new ElementFilter() { + @Override + public boolean isAcceptable(Object element, @Nullable PsiElement context) { + if (element instanceof JsonValue) { + if (!isSchemaFile((PsiElement)element)) return false; + if (((JsonValue)element).getParent() instanceof JsonProperty && + ((JsonProperty)((JsonValue)element).getParent()).getValue() == element) { + return propertyName.equals(((JsonProperty)((JsonValue)element).getParent()).getName()); + } + } + return false; + } + + @Override + public boolean isClassAcceptable(Class hintClass) { + return true; + } + })); + } + + private static boolean isSchemaFile(@NotNull final PsiElement element) { + final Project project = element.getProject(); + final PsiFile file = element.getContainingFile(); + if (!JsonSchemaFileType.INSTANCE.equals(file.getFileType())) return false; + return file.getVirtualFile() != null && + JsonSchemaMappingsProjectConfiguration.getInstance(project).isRegisteredSchemaFile(file.getVirtualFile()); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/ide/JsonSchemaService.java b/json/src/com/jetbrains/jsonSchema/ide/JsonSchemaService.java index c004d80216da..c04352faf63e 100644 --- a/json/src/com/jetbrains/jsonSchema/ide/JsonSchemaService.java +++ b/json/src/com/jetbrains/jsonSchema/ide/JsonSchemaService.java @@ -9,6 +9,7 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.Consumer; +import com.jetbrains.jsonSchema.impl.JsonSchemaServiceEx; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -18,12 +19,12 @@ import java.util.List; public interface JsonSchemaService { class Impl { - - @Nullable public static JsonSchemaService get(@NotNull Project project) { return ServiceManager.getService(project, JsonSchemaService.class); } - + public static JsonSchemaServiceEx getEx(@NotNull Project project) { + return (JsonSchemaServiceEx) ServiceManager.getService(project, JsonSchemaService.class); + } } @Nullable diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaExportedDefinitions.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaExportedDefinitions.java index 58c84f9fe35c..bc4409cc9a17 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaExportedDefinitions.java +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaExportedDefinitions.java @@ -15,16 +15,17 @@ */ package com.jetbrains.jsonSchema.impl; -import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.Consumer; import com.intellij.util.PairConsumer; import com.intellij.util.containers.BidirectionalMap; import com.intellij.util.containers.MultiMap; +import com.jetbrains.jsonSchema.extension.JsonSchemaProjectSelfProviderFactory; import com.jetbrains.jsonSchema.extension.SchemaType; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import java.io.File; import java.util.*; import static com.jetbrains.jsonSchema.impl.JsonSchemaReader.LOG; @@ -39,12 +40,9 @@ public class JsonSchemaExportedDefinitions { private final BidirectionalMap> myId2Key; private final MultiMap, Pair> myCrossDependencies; private final Map> myMap; - private final Project myProject; @NotNull private final Consumer, Consumer>>> mySchemasIterator; - public JsonSchemaExportedDefinitions(@Nullable final Project project, - @NotNull Consumer, Consumer>>> schemasIterator) { - myProject = project; + public JsonSchemaExportedDefinitions(@NotNull Consumer, Consumer>>> schemasIterator) { mySchemasIterator = schemasIterator; myLock = new Object(); myMap = new HashMap<>(); @@ -131,4 +129,19 @@ public class JsonSchemaExportedDefinitions { } return dirtyKeys; } + + public boolean checkFileForId(@NotNull final String id, @NotNull final VirtualFile file) { + final Pair pair; + synchronized (myLock) { + ensureInitialized(); + pair = myId2Key.get(id); + } + if (pair == null) return false; + if (SchemaType.schema.equals(pair.getFirst())) return JsonSchemaProjectSelfProviderFactory.SCHEMA_JSON_FILE_NAME + .equals(file.getName()); + if (SchemaType.embeddedSchema.equals(pair.getFirst())) return file.getName().equals(pair.getSecond()); + if (SchemaType.userSchema.equals(pair.getFirst())) return pair.getSecond() != null && + pair.getSecond().equals(new File(file.getPath())); + return false; + } } diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java index ee4425e15085..f24ae17dfa61 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java @@ -8,7 +8,7 @@ import java.util.Map; /** * @author Irina.Chernushina on 8/28/2015. */ -class JsonSchemaObject { +public class JsonSchemaObject { private Map myDefinitions; private Map myProperties; private Map myPatternProperties; diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java index 2a54a8b211d0..55db02281b87 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java @@ -162,7 +162,7 @@ public class JsonSchemaReader { } @Nullable - static JsonSchemaObject findDefinition(@NotNull Pair key, + public static JsonSchemaObject findDefinition(@NotNull Pair key, @NotNull String ref, @NotNull final JsonSchemaObject root, @NotNull final Map ids, diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceEx.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceEx.java new file mode 100644 index 000000000000..8c566a5eebaa --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceEx.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.openapi.vfs.VirtualFile; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import org.jetbrains.annotations.NotNull; + +/** + * @author Irina.Chernushina on 4/1/2016. + */ +public interface JsonSchemaServiceEx extends JsonSchemaService { + + boolean checkFileForId(@NotNull String id, @NotNull VirtualFile file); +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceImpl.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceImpl.java index c8d1eee4f328..b91811d3eb3f 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceImpl.java +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceImpl.java @@ -29,7 +29,6 @@ import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider; import com.jetbrains.jsonSchema.extension.JsonSchemaImportedProviderMarker; import com.jetbrains.jsonSchema.extension.JsonSchemaProviderFactory; import com.jetbrains.jsonSchema.extension.SchemaType; -import com.jetbrains.jsonSchema.ide.JsonSchemaService; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -38,7 +37,7 @@ import java.io.IOException; import java.io.Reader; import java.util.*; -public class JsonSchemaServiceImpl implements JsonSchemaService { +public class JsonSchemaServiceImpl implements JsonSchemaServiceEx { private static final Logger LOGGER = Logger.getInstance(JsonSchemaServiceImpl.class); private static final Logger RARE_LOGGER = RareLogger.wrap(LOGGER, false); @Nullable @@ -50,8 +49,8 @@ public class JsonSchemaServiceImpl implements JsonSchemaService { public JsonSchemaServiceImpl(@Nullable Project project) { myLock = new Object(); myProject = project; - myDefinitions = new JsonSchemaExportedDefinitions(project, - new Consumer, Consumer>>>() { + myDefinitions = new JsonSchemaExportedDefinitions( + new Consumer, Consumer>>>() { @Override public void consume(PairConsumer, Consumer>> consumer) { iterateSchemas(consumer); @@ -147,6 +146,13 @@ public class JsonSchemaServiceImpl implements JsonSchemaService { } catch (Exception e) { logException(provider, e); + } finally { + if (reader != null) try { + reader.close(); + } + catch (IOException e) { + logException(provider, e); + } } return null; } @@ -303,4 +309,9 @@ public class JsonSchemaServiceImpl implements JsonSchemaService { return myDocumentationProvider; } } + + @Override + public boolean checkFileForId(@NotNull final String id, @NotNull final VirtualFile file) { + return myDefinitions.checkFileForId(id, file); + } } diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaCrossReferencesTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaCrossReferencesTest.java index df5f0c3df88f..4eaa2d92e9db 100644 --- a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaCrossReferencesTest.java +++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaCrossReferencesTest.java @@ -21,9 +21,15 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.application.ex.PathManagerEx; import com.intellij.openapi.editor.Document; +import com.intellij.openapi.extensions.AreaPicoContainer; +import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.fileTypes.FileTypeManager; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.schemaFile.TestJsonSchemaMappingsProjectConfiguration; import org.junit.Assert; import java.util.Collections; @@ -33,6 +39,15 @@ import java.util.Collections; */ public class JsonSchemaCrossReferencesTest extends CompletionTestCase { private final static String BASE_PATH = "/tests/testData/jsonSchema/crossReferences"; + private final static String BASE_SCHEMA_RESOLVE_PATH = "/tests/testData/jsonSchema/schemaFile/resolve"; + + private FileTypeManager myFileTypeManager; + + @Override + public void setUp() throws Exception { + super.setUp(); + myFileTypeManager = FileTypeManager.getInstance(); + } @Override protected String getTestDataPath() { @@ -173,4 +188,63 @@ public class JsonSchemaCrossReferencesTest extends CompletionTestCase { complete(); assertStringItems("\"one1\"", "\"two1\""); } + + public void testJsonSchemaRefsCrossResolve() throws Exception { + configureByFiles(null, BASE_SCHEMA_RESOLVE_PATH + "/referencingSchema.json", BASE_SCHEMA_RESOLVE_PATH + "/localRefSchema.json"); + + String moduleDir = null; + VirtualFile moduleFile = null; + VirtualFile[] children = getProject().getBaseDir().getChildren(); + for (VirtualFile child : children) { + if (child.isDirectory()) { + moduleDir = child.getName(); + moduleFile = child; + break; + } + } + Assert.assertNotNull(moduleDir); + + AreaPicoContainer container = Extensions.getArea(getProject()).getPicoContainer(); + final String key = JsonSchemaMappingsProjectConfiguration.class.getName(); + container.unregisterComponent(key); + container.registerComponentImplementation(key, TestJsonSchemaMappingsProjectConfiguration.class); + + final JsonSchemaMappingsProjectConfiguration instance = JsonSchemaMappingsProjectConfiguration.getInstance(getProject()); + final JsonSchemaMappingsConfigurationBase.SchemaInfo base = + new JsonSchemaMappingsConfigurationBase.SchemaInfo("base", "/" + moduleDir + "/localRefSchema.json", false, Collections.emptyList()); + instance.addSchema(base); + + final JsonSchemaMappingsConfigurationBase.SchemaInfo inherited + = new JsonSchemaMappingsConfigurationBase.SchemaInfo("inherited", "/" + moduleDir + "/referencingSchema.json", false, Collections.emptyList()); + + instance.addSchema(inherited); + + try { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + myFileTypeManager.associatePattern(JsonSchemaFileType.INSTANCE, "*Schema.json"); + } + }); + int offset = myEditor.getCaretModel().getPrimaryCaret().getOffset(); + final PsiReference referenceAt = myFile.findReferenceAt(offset); + Assert.assertNotNull(referenceAt); + final PsiElement resolve = referenceAt.resolve(); + Assert.assertNotNull(resolve); + Assert.assertEquals("\"baseEnum\"", resolve.getText()); + + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + myFileTypeManager.removeAssociatedExtension(JsonSchemaFileType.INSTANCE, "*Schema.json"); + } + }); + } finally { + container.unregisterComponent(key); + container.registerComponentImplementation(key, JsonSchemaMappingsProjectConfiguration.class); + } + + instance.removeSchema(inherited); + instance.removeSchema(base); + } } diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestProvider.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestProvider.java index 45515ec849f4..7880a2a6c6a2 100644 --- a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestProvider.java +++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestProvider.java @@ -1,7 +1,8 @@ package com.jetbrains.jsonSchema; -import com.intellij.json.JsonFileType; +import com.intellij.json.JsonLanguage; +import com.intellij.openapi.fileTypes.LanguageFileType; import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VirtualFile; import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider; @@ -22,7 +23,7 @@ public class JsonSchemaTestProvider implements JsonSchemaFileProvider { @Override public boolean isAvailable(@NotNull VirtualFile file) { - return file.getFileType() == JsonFileType.INSTANCE; + return file.getFileType() instanceof LanguageFileType && ((LanguageFileType)file.getFileType()).getLanguage().isKindOf(JsonLanguage.INSTANCE); } @Nullable diff --git a/json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaFileResolveTest.java b/json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaFileResolveTest.java new file mode 100644 index 000000000000..5f3199ac80e7 --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaFileResolveTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed 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 com.jetbrains.jsonSchema.schemaFile; + +import com.intellij.openapi.application.PathManager; +import com.intellij.openapi.application.ex.PathManagerEx; +import com.intellij.openapi.extensions.AreaPicoContainer; +import com.intellij.openapi.extensions.Extensions; +import com.intellij.openapi.fileTypes.FileTypeManager; +import com.intellij.openapi.fileTypes.ex.FileTypeManagerEx; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase; +import com.jetbrains.jsonSchema.JsonSchemaFileType; +import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration; +import org.junit.Assert; + +/** + * @author Irina.Chernushina on 4/1/2016. + */ +public class JsonSchemaFileResolveTest extends LightPlatformCodeInsightFixtureTestCase { + private final static String BASE_PATH = "/tests/testData/jsonSchema/schemaFile/resolve"; + private FileTypeManager myFileTypeManager; + + @Override + public void setUp() throws Exception { + super.setUp(); + myFileTypeManager = FileTypeManagerEx.getInstanceEx(); + } + + @Override + protected String getTestDataPath() { + PathManagerEx.TestDataLookupStrategy strategy = PathManagerEx.guessTestDataLookupStrategy(); + if (strategy.equals(PathManagerEx.TestDataLookupStrategy.COMMUNITY)) { + return PathManager.getHomePath() + "/json" + BASE_PATH; + } + return PathManager.getHomePath() + "/community/json" + BASE_PATH; + } + + public void testResolveLocalRef() throws Exception { + AreaPicoContainer container = Extensions.getArea(getProject()).getPicoContainer(); + final String key = JsonSchemaMappingsProjectConfiguration.class.getName(); + container.unregisterComponent(key); + container.registerComponentImplementation(key, TestJsonSchemaMappingsProjectConfiguration.class); + + try { + myFileTypeManager.associatePattern(JsonSchemaFileType.INSTANCE, "*Schema.json"); + PsiReference position = myFixture.getReferenceAtCaretPosition("localRefSchema.json"); + Assert.assertNotNull(position); + PsiElement resolve = position.resolve(); + Assert.assertNotNull(resolve); + Assert.assertEquals("\"baseEnum\"", resolve.getText()); + + myFileTypeManager.removeAssociatedExtension(JsonSchemaFileType.INSTANCE, "*Schema.json"); + } finally { + container.unregisterComponent(key); + container.registerComponentImplementation(key, JsonSchemaMappingsProjectConfiguration.class); + } + } + + public void testResolveExternalRef() throws Exception { + AreaPicoContainer container = Extensions.getArea(getProject()).getPicoContainer(); + final String key = JsonSchemaMappingsProjectConfiguration.class.getName(); + container.unregisterComponent(key); + container.registerComponentImplementation(key, TestJsonSchemaMappingsProjectConfiguration.class); + + try { + myFileTypeManager.associatePattern(JsonSchemaFileType.INSTANCE, "*Schema.json"); + PsiReference position = myFixture.getReferenceAtCaretPosition("localRefSchema.json"); + Assert.assertNotNull(position); + PsiElement resolve = position.resolve(); + Assert.assertNotNull(resolve); + Assert.assertEquals("\"baseEnum\"", resolve.getText()); + + myFileTypeManager.removeAssociatedExtension(JsonSchemaFileType.INSTANCE, "*Schema.json"); + } finally { + container.unregisterComponent(key); + container.registerComponentImplementation(key, JsonSchemaMappingsProjectConfiguration.class); + } + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/schemaFile/TestJsonSchemaMappingsProjectConfiguration.java b/json/tests/test/com/jetbrains/jsonSchema/schemaFile/TestJsonSchemaMappingsProjectConfiguration.java new file mode 100644 index 000000000000..51b8aca07c0d --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/schemaFile/TestJsonSchemaMappingsProjectConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed 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 com.jetbrains.jsonSchema.schemaFile; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration; +import org.jetbrains.annotations.NotNull; + +/** + * @author Irina.Chernushina on 4/1/2016. + */ +public class TestJsonSchemaMappingsProjectConfiguration extends JsonSchemaMappingsProjectConfiguration { + public TestJsonSchemaMappingsProjectConfiguration(Project project) { + super(project); + } + + @Override + public boolean isRegisteredSchemaFile(@NotNull VirtualFile file) { + return true; + } +} diff --git a/json/tests/testData/jsonSchema/schemaFile/resolve/localRefSchema.json b/json/tests/testData/jsonSchema/schemaFile/resolve/localRefSchema.json new file mode 100644 index 000000000000..c053edda979d --- /dev/null +++ b/json/tests/testData/jsonSchema/schemaFile/resolve/localRefSchema.json @@ -0,0 +1,12 @@ +{ + "id": "https://myurl", + "properties": { + "baseEnum": { + "type": "string", + "enum": ["one", "two"] + }, + "smth": { + "$ref" : "#/properties/baseEnum" + } + } +} \ No newline at end of file diff --git a/json/tests/testData/jsonSchema/schemaFile/resolve/referencingSchema.json b/json/tests/testData/jsonSchema/schemaFile/resolve/referencingSchema.json new file mode 100644 index 000000000000..8a890fcb1f3e --- /dev/null +++ b/json/tests/testData/jsonSchema/schemaFile/resolve/referencingSchema.json @@ -0,0 +1,7 @@ +{ + "properties": { + "smth": { + "$ref" : "https://myurl#/properties/baseEnum" + } + } +} \ No newline at end of file diff --git a/platform/platform-resources/src/META-INF/JsonPlugin.xml b/platform/platform-resources/src/META-INF/JsonPlugin.xml index c553ac02ae7b..9f50e0b13dbb 100644 --- a/platform/platform-resources/src/META-INF/JsonPlugin.xml +++ b/platform/platform-resources/src/META-INF/JsonPlugin.xml @@ -16,6 +16,7 @@ + @@ -73,6 +74,9 @@ + + +