summaryrefslogtreecommitdiff
path: root/json/src
diff options
context:
space:
mode:
authorAndrej Shadura <andrew.shadura@collabora.co.uk>2019-08-28 14:13:29 +0200
committerAndrej Shadura <andrew.shadura@collabora.co.uk>2019-08-29 17:48:13 +0200
commite19ef5983707e6a5c8d127f1ac8f02754cef82fd (patch)
tree9e3852cb9abc81ed6aa444465928d45fd7763dea /json/src
New upstream version 0~183.5153.4+dfsg
Diffstat (limited to 'json/src')
-rw-r--r--json/src/com/intellij/json/JsonBraceMatcher.java34
-rw-r--r--json/src/com/intellij/json/JsonBundle.java36
-rw-r--r--json/src/com/intellij/json/JsonBundle.properties66
-rw-r--r--json/src/com/intellij/json/JsonDialectUtil.java25
-rw-r--r--json/src/com/intellij/json/JsonElementType.java11
-rw-r--r--json/src/com/intellij/json/JsonFileType.java50
-rw-r--r--json/src/com/intellij/json/JsonFileTypeFactory.java15
-rw-r--r--json/src/com/intellij/json/JsonLanguage.java22
-rw-r--r--json/src/com/intellij/json/JsonLexer.java12
-rw-r--r--json/src/com/intellij/json/JsonNamesValidator.java38
-rw-r--r--json/src/com/intellij/json/JsonParserDefinition.java83
-rw-r--r--json/src/com/intellij/json/JsonQuoteHandler.java40
-rw-r--r--json/src/com/intellij/json/JsonSpellcheckerStrategy.java87
-rw-r--r--json/src/com/intellij/json/JsonTokenType.java11
-rw-r--r--json/src/com/intellij/json/JsonUtil.java94
-rw-r--r--json/src/com/intellij/json/_JsonLexer.flex58
-rw-r--r--json/src/com/intellij/json/breadcrumbs/JsonBreadcrumbsProvider.java73
-rw-r--r--json/src/com/intellij/json/codeinsight/JsonCompletionContributor.java61
-rw-r--r--json/src/com/intellij/json/codeinsight/JsonDuplicatePropertyKeysInspection.java156
-rw-r--r--json/src/com/intellij/json/codeinsight/JsonLiteralAnnotator.java93
-rw-r--r--json/src/com/intellij/json/codeinsight/JsonLiteralChecker.java21
-rw-r--r--json/src/com/intellij/json/codeinsight/JsonStandardComplianceInspection.java234
-rw-r--r--json/src/com/intellij/json/codeinsight/JsonStandardComplianceProvider.java44
-rw-r--r--json/src/com/intellij/json/codeinsight/JsonStringPropertyInsertHandler.java84
-rw-r--r--json/src/com/intellij/json/codeinsight/StandardJsonLiteralChecker.java73
-rw-r--r--json/src/com/intellij/json/editor/JsonCommenter.java41
-rw-r--r--json/src/com/intellij/json/editor/JsonCopyPastePostProcessor.java169
-rw-r--r--json/src/com/intellij/json/editor/JsonCopyPasteProcessor.java72
-rw-r--r--json/src/com/intellij/json/editor/JsonEditorOptions.java38
-rw-r--r--json/src/com/intellij/json/editor/JsonEnterHandler.java147
-rw-r--r--json/src/com/intellij/json/editor/JsonSmartKeysConfigurable.java42
-rw-r--r--json/src/com/intellij/json/editor/JsonTypedHandler.java142
-rw-r--r--json/src/com/intellij/json/editor/folding/JsonFoldingBuilder.java110
-rw-r--r--json/src/com/intellij/json/editor/lineMover/JsonLineMover.java197
-rw-r--r--json/src/com/intellij/json/editor/selection/JsonBasicWordSelectionFilter.java16
-rw-r--r--json/src/com/intellij/json/editor/selection/JsonStringLiteralSelectionHandler.java43
-rw-r--r--json/src/com/intellij/json/editor/smartEnter/JsonSmartEnterProcessor.java123
-rw-r--r--json/src/com/intellij/json/findUsages/JsonFindUsagesProvider.java54
-rw-r--r--json/src/com/intellij/json/findUsages/JsonWordScanner.java19
-rw-r--r--json/src/com/intellij/json/formatter/JsonBlock.java221
-rw-r--r--json/src/com/intellij/json/formatter/JsonCodeStyleSettings.java78
-rw-r--r--json/src/com/intellij/json/formatter/JsonCodeStyleSettingsProvider.java57
-rw-r--r--json/src/com/intellij/json/formatter/JsonFormattingBuilderModel.java42
-rw-r--r--json/src/com/intellij/json/formatter/JsonLanguageCodeStyleSettingsProvider.java116
-rw-r--r--json/src/com/intellij/json/formatter/JsonLineWrapPositionStrategy.java70
-rw-r--r--json/src/com/intellij/json/formatter/JsonTrailingCommaRemover.java111
-rw-r--r--json/src/com/intellij/json/highlighting/JsonColorsPage.java105
-rw-r--r--json/src/com/intellij/json/highlighting/JsonSyntaxHighlighterFactory.java164
-rw-r--r--json/src/com/intellij/json/json5/Json5FileType.java32
-rw-r--r--json/src/com/intellij/json/json5/Json5FileTypeFactory.java13
-rw-r--r--json/src/com/intellij/json/json5/Json5Language.java17
-rw-r--r--json/src/com/intellij/json/json5/Json5Lexer.java10
-rw-r--r--json/src/com/intellij/json/json5/Json5ParserDefinition.java31
-rw-r--r--json/src/com/intellij/json/json5/Json5PsiWalkerFactory.java36
-rw-r--r--json/src/com/intellij/json/json5/_Json5Lexer.flex64
-rw-r--r--json/src/com/intellij/json/json5/codeinsight/Json5JsonLiteralChecker.java52
-rw-r--r--json/src/com/intellij/json/json5/codeinsight/Json5StandardComplianceInspection.java81
-rw-r--r--json/src/com/intellij/json/json5/highlighting/Json5SyntaxHighlightingFactory.java20
-rw-r--r--json/src/com/intellij/json/liveTemplates/JsonContextType.java22
-rw-r--r--json/src/com/intellij/json/liveTemplates/JsonInLiteralsContextType.java21
-rw-r--r--json/src/com/intellij/json/liveTemplates/JsonInPropertyKeysContextType.java33
-rw-r--r--json/src/com/intellij/json/navigation/JsonQualifiedNameKind.java20
-rw-r--r--json/src/com/intellij/json/navigation/JsonQualifiedNameProvider.java69
-rw-r--r--json/src/com/intellij/json/psi/JsonElement.java10
-rw-r--r--json/src/com/intellij/json/psi/JsonElementGenerator.java79
-rw-r--r--json/src/com/intellij/json/psi/JsonFile.java23
-rw-r--r--json/src/com/intellij/json/psi/JsonParserUtil.java25
-rw-r--r--json/src/com/intellij/json/psi/JsonPsiChangeUtils.java42
-rw-r--r--json/src/com/intellij/json/psi/JsonPsiUtil.java231
-rw-r--r--json/src/com/intellij/json/psi/JsonStringLiteralManipulator.java32
-rw-r--r--json/src/com/intellij/json/psi/impl/JSStringLiteralEscaper.java194
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonElementImpl.java23
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonFileImpl.java43
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonLiteralMixin.java22
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonObjectMixin.java56
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonPropertyMixin.java42
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonPropertyNameReference.java74
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonPsiImplUtils.java233
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonRecursiveElementVisitor.java17
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonStringLiteralMixin.java45
-rw-r--r--json/src/com/intellij/json/psi/impl/JsonTreeChangePreprocessor.java40
-rw-r--r--json/src/com/intellij/json/structureView/JsonStructureViewBuilderFactory.java32
-rw-r--r--json/src/com/intellij/json/structureView/JsonStructureViewElement.java86
-rw-r--r--json/src/com/intellij/json/structureView/JsonStructureViewModel.java37
-rw-r--r--json/src/com/intellij/json/surroundWith/JsonSurroundDescriptor.java84
-rw-r--r--json/src/com/intellij/json/surroundWith/JsonSurrounderBase.java57
-rw-r--r--json/src/com/intellij/json/surroundWith/JsonWithArrayLiteralSurrounder.java18
-rw-r--r--json/src/com/intellij/json/surroundWith/JsonWithObjectLiteralSurrounder.java85
-rw-r--r--json/src/com/intellij/json/surroundWith/JsonWithQuotesSurrounder.java19
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonMappingKind.java41
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonPointerResolver.java60
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonPointerUtil.java38
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogConfigurable.java110
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogProjectConfiguration.java86
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonSchemaFileType.java67
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonSchemaIconProvider.java40
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonSchemaMappingsProjectConfiguration.java137
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonSchemaRefactoringListenerProvider.java67
-rw-r--r--json/src/com/jetbrains/jsonSchema/JsonSchemaVfsListener.java115
-rw-r--r--json/src/com/jetbrains/jsonSchema/UserDefinedJsonSchemaConfiguration.java323
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalker.java81
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalkerFactory.java33
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonSchemaEnabler.java31
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonSchemaFileProvider.java37
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonSchemaImportedProviderMarker.java22
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonSchemaInfo.java132
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProjectSelfProviderFactory.java125
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProviderFactory.java42
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonSchemaUserDefinedProviderFactory.java138
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/JsonWidgetSuppressor.java17
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/SchemaType.java23
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/adapters/JsonArrayValueAdapter.java32
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/adapters/JsonObjectValueAdapter.java32
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/adapters/JsonPropertyAdapter.java33
-rw-r--r--json/src/com/jetbrains/jsonSchema/extension/adapters/JsonValueAdapter.java40
-rw-r--r--json/src/com/jetbrains/jsonSchema/ide/JsonSchemaService.java75
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/CachedValueProviderOnPsiFile.java41
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/EnumArrayValueWrapper.java25
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/EnumObjectValueWrapper.java25
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonCachedValues.java192
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonComplianceCheckerOptions.java13
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonErrorPriority.java10
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonOriginalPsiWalker.java217
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonPointerReferenceProvider.java262
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonRequiredPropsReferenceProvider.java62
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java1147
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaBaseReference.java58
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaCompletionContributor.java672
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaComplianceChecker.java157
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaConflictNotificationProvider.java120
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaDocumentationProvider.java218
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndex.java171
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaGotoDeclarationHandler.java48
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaInJsonFilesEnabler.java13
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java1180
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java511
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReferenceContributor.java94
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaRegexInjector.java68
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaResolver.java144
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceImpl.java488
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaTreeNode.java193
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaType.java84
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaUsageTriggerCollector.java13
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVariantsTreeBuilder.java595
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVersion.java60
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/JsonValidationError.java163
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/MatchResult.java74
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/SchemaResolveState.java23
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonArrayAdapter.java86
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonGenericValueAdapter.java81
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonObjectAdapter.java86
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonPropertyAdapter.java79
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/fixes/AddMissingPropertyFix.java156
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/fixes/RemoveProhibitedPropertyFix.java46
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/fixes/SuggestEnumValuesFix.java89
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaBasedInspectionBase.java46
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaComplianceInspection.java67
-rw-r--r--json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaRefReferenceInspection.java93
-rw-r--r--json/src/com/jetbrains/jsonSchema/remote/JsonFileResolver.java92
-rw-r--r--json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogExclusion.java19
-rw-r--r--json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogManager.java173
-rw-r--r--json/src/com/jetbrains/jsonSchema/remote/JsonSchemaRemoteContentProvider.java125
-rw-r--r--json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableCellEditor.java150
-rw-r--r--json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableView.java52
-rw-r--r--json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaConfigurable.java219
-rw-r--r--json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsConfigurable.java333
-rw-r--r--json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsView.java339
-rw-r--r--json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaPatternComparator.java104
-rw-r--r--json/src/com/jetbrains/jsonSchema/settings/mappings/TreeUpdater.java6
-rw-r--r--json/src/com/jetbrains/jsonSchema/widget/JsonSchemaInfoPopupStep.java169
-rw-r--r--json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusPopup.java82
-rw-r--r--json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidget.java310
-rw-r--r--json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidgetProvider.java16
-rw-r--r--json/src/jsonSchema/build.xml126
-rw-r--r--json/src/jsonSchema/schema.json154
-rw-r--r--json/src/jsonSchema/schema06.json158
-rw-r--r--json/src/jsonSchema/schema07.json172
177 files changed, 18614 insertions, 0 deletions
diff --git a/json/src/com/intellij/json/JsonBraceMatcher.java b/json/src/com/intellij/json/JsonBraceMatcher.java
new file mode 100644
index 00000000..d4a8aad3
--- /dev/null
+++ b/json/src/com/intellij/json/JsonBraceMatcher.java
@@ -0,0 +1,34 @@
+package com.intellij.json;
+
+import com.intellij.lang.BracePair;
+import com.intellij.lang.PairedBraceMatcher;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonBraceMatcher implements PairedBraceMatcher {
+ private static final BracePair[] PAIRS = {
+ new BracePair(JsonElementTypes.L_BRACKET, JsonElementTypes.R_BRACKET, true),
+ new BracePair(JsonElementTypes.L_CURLY, JsonElementTypes.R_CURLY, true)
+ };
+
+ @NotNull
+ @Override
+ public BracePair[] getPairs() {
+ return PAIRS;
+ }
+
+ @Override
+ public boolean isPairedBracesAllowedBeforeType(@NotNull IElementType lbraceType, @Nullable IElementType contextType) {
+ return true;
+ }
+
+ @Override
+ public int getCodeConstructStart(PsiFile file, int openingBraceOffset) {
+ return openingBraceOffset;
+ }
+}
diff --git a/json/src/com/intellij/json/JsonBundle.java b/json/src/com/intellij/json/JsonBundle.java
new file mode 100644
index 00000000..9ccab90f
--- /dev/null
+++ b/json/src/com/intellij/json/JsonBundle.java
@@ -0,0 +1,36 @@
+package com.intellij.json;
+
+import com.intellij.CommonBundle;
+import com.intellij.reference.SoftReference;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.PropertyKey;
+
+import java.lang.ref.Reference;
+import java.util.ResourceBundle;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonBundle {
+
+ private static Reference<ResourceBundle> ourBundle;
+ @NonNls public static final String BUNDLE = "com.intellij.json.JsonBundle";
+
+ private JsonBundle() {
+ // empty
+ }
+
+ public static String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, @NotNull Object... params) {
+ return CommonBundle.message(getBundle(), key, params);
+ }
+
+ private static ResourceBundle getBundle() {
+ ResourceBundle bundle = SoftReference.dereference(ourBundle);
+ if (bundle == null) {
+ bundle = ResourceBundle.getBundle(BUNDLE);
+ ourBundle = new SoftReference<>(bundle);
+ }
+ return bundle;
+ }
+}
diff --git a/json/src/com/intellij/json/JsonBundle.properties b/json/src/com/intellij/json/JsonBundle.properties
new file mode 100644
index 00000000..2089ac9a
--- /dev/null
+++ b/json/src/com/intellij/json/JsonBundle.properties
@@ -0,0 +1,66 @@
+json.array=array
+json.object=object
+json.property=property
+
+syntax.error.missing.closing.quote=Missing closing quote
+syntax.error.illegal.escape.sequence=Illegal escape sequence
+syntax.error.illegal.unicode.escape.sequence=Illegal unicode escape sequence
+syntax.error.illegal.floating.point.literal=Illegal floating point literal
+syntax.error.control.char.in.string=Control character ''{0}'' is not allowed in string literals
+
+# Inspections
+json.inspection.group=JSON and JSON5
+
+inspection.compliance.name=Compliance with JSON standard
+inspection.compliance5.name=Compliance with JSON5 standard
+inspection.compliance.msg.comments=JSON standard does not allow comments. Use JSMin or similar tool to remove comments before parsing.
+inspection.compliance.msg.single.quoted.strings=JSON standard does not allow single quoted strings
+inspection.compliance.msg.bad.token=JSON standard does not allow such tokens
+inspection.compliance.msg.illegal.property.key=JSON standard allows only double quoted string as property key
+inspection.compliance.msg.trailing.comma=JSON standard does not allow trailing comma
+inspection.compliance.msg.multiple.top.level.values=JSON standard allows only one top-level value
+
+inspection.compliance.option.comments=Warn about comments
+inspection.compliance.option.multiple.top.level.values=Warn about multiple top-level values
+inspection.compliance.option.trailing.comma=Warn about trailing commas
+inspection.compliance.option.nan.infinity=Warn about NaN and Infinity/-Infinity numeric values
+
+inspection.duplicate.keys.name=Duplicate keys in object literals
+inspection.duplicate.keys.msg.duplicate.keys=Object contains duplicate keys ''{0}''
+
+# Formatter
+formatter.align.properties.caption=Align
+
+formatter.align.properties.none=Do not align
+formatter.align.properties.on.colon=On colon
+formatter.align.properties.on.value=On value
+
+# Quickfixes and editor actions
+quickfix.add.double.quotes.desc=Wrap with double quotes
+
+surround.with.object.literal.desc=object literal
+surround.with.array.literal.desc=array literal
+surround.with.quotes.desc=quotes
+json.template.context.type=JSON
+
+json.copy.to.clipboard=Copy {0} to clipboard
+
+#json schema
+json.schema.add.schema.chooser.title=Select JSON Schema File
+json.schema.annotation.not.allowed.property=Property ''{0}'' is not allowed
+json.schema.conflicting.mappings=Warning: conflicting mappings. <a href="#">Show details</a>
+json.schema.file.selector.title=Schema file or URL:
+json.schema.file.not.found=File not found
+json.schema.inspection.compliance.name=Compliance with JSON schema
+json.schema.inspection.case.insensitive.enum=Case insensitive matching for enum values
+
+json.schema.ref.refs.inspection.name=Unresolved '$ref' and '$schema' references
+json.schema.ref.file.not.found=File ''{0}'' not found
+json.schema.ref.cannot.resolve.path=Cannot resolve path ''{0}''
+json.schema.ref.cannot.resolve.id=Cannot resolve id ''{0}''
+json.schema.ref.no.array.element=Array doesn''t contain element with index '{0}'
+json.schema.ref.no.property=Property ''{0}'' not found
+
+settings.json.schema.add.mapping=Add mapping
+settings.json.schema.edit.mapping=Edit mapping
+settings.json.schema.remove.mapping=Remove mapping \ No newline at end of file
diff --git a/json/src/com/intellij/json/JsonDialectUtil.java b/json/src/com/intellij/json/JsonDialectUtil.java
new file mode 100644
index 00000000..610e3f99
--- /dev/null
+++ b/json/src/com/intellij/json/JsonDialectUtil.java
@@ -0,0 +1,25 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json;
+
+import com.intellij.lang.Language;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class JsonDialectUtil {
+ public static boolean isStandardJson(@NotNull PsiElement element) {
+ return isStandardJson(getLanguage(element));
+ }
+
+ public static Language getLanguage(@NotNull PsiElement element) {
+ PsiFile file = element.getContainingFile();
+ if (file == null) return JsonLanguage.INSTANCE;
+ Language language = file.getLanguage();
+ return language instanceof JsonLanguage ? language : JsonLanguage.INSTANCE;
+ }
+
+ public static boolean isStandardJson(@Nullable Language language) {
+ return language == JsonLanguage.INSTANCE;
+ }
+}
diff --git a/json/src/com/intellij/json/JsonElementType.java b/json/src/com/intellij/json/JsonElementType.java
new file mode 100644
index 00000000..54e05dfc
--- /dev/null
+++ b/json/src/com/intellij/json/JsonElementType.java
@@ -0,0 +1,11 @@
+package com.intellij.json;
+
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonElementType extends IElementType {
+ public JsonElementType(@NotNull @NonNls String debugName) {
+ super(debugName, JsonLanguage.INSTANCE);
+ }
+}
diff --git a/json/src/com/intellij/json/JsonFileType.java b/json/src/com/intellij/json/JsonFileType.java
new file mode 100644
index 00000000..9b891800
--- /dev/null
+++ b/json/src/com/intellij/json/JsonFileType.java
@@ -0,0 +1,50 @@
+package com.intellij.json;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.lang.Language;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonFileType extends LanguageFileType{
+ public static final JsonFileType INSTANCE = new JsonFileType();
+ public static final String DEFAULT_EXTENSION = "json";
+
+ protected JsonFileType(Language language) {
+ super(language);
+ }
+
+ public JsonFileType() {
+ super(JsonLanguage.INSTANCE);
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return "JSON";
+ }
+
+ @NotNull
+ @Override
+ public String getDescription() {
+ return "JSON";
+ }
+
+ @NotNull
+ @Override
+ public String getDefaultExtension() {
+ return DEFAULT_EXTENSION;
+ }
+
+ @Nullable
+ @Override
+ public Icon getIcon() {
+ // TODO: add JSON icon instead
+ return AllIcons.FileTypes.Json;
+ }
+}
diff --git a/json/src/com/intellij/json/JsonFileTypeFactory.java b/json/src/com/intellij/json/JsonFileTypeFactory.java
new file mode 100644
index 00000000..2cfaa2f1
--- /dev/null
+++ b/json/src/com/intellij/json/JsonFileTypeFactory.java
@@ -0,0 +1,15 @@
+package com.intellij.json;
+
+import com.intellij.openapi.fileTypes.FileTypeConsumer;
+import com.intellij.openapi.fileTypes.FileTypeFactory;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonFileTypeFactory extends FileTypeFactory {
+ @Override
+ public void createFileTypes(@NotNull FileTypeConsumer consumer) {
+ consumer.consume(JsonFileType.INSTANCE, JsonFileType.DEFAULT_EXTENSION);
+ }
+}
diff --git a/json/src/com/intellij/json/JsonLanguage.java b/json/src/com/intellij/json/JsonLanguage.java
new file mode 100644
index 00000000..e43ac474
--- /dev/null
+++ b/json/src/com/intellij/json/JsonLanguage.java
@@ -0,0 +1,22 @@
+package com.intellij.json;
+
+import com.intellij.lang.Language;
+
+public class JsonLanguage extends Language {
+ public static final JsonLanguage INSTANCE = new JsonLanguage();
+
+ protected JsonLanguage(String ID, String... mimeTypes) {
+ super(INSTANCE, ID, mimeTypes);
+ }
+
+ private JsonLanguage() {
+ super("JSON", "application/json", "application/vnd.api+json", "application/hal+json");
+ }
+
+ @Override
+ public boolean isCaseSensitive() {
+ return true;
+ }
+
+ public boolean hasPermissiveStrings() { return false; }
+}
diff --git a/json/src/com/intellij/json/JsonLexer.java b/json/src/com/intellij/json/JsonLexer.java
new file mode 100644
index 00000000..ce771b9d
--- /dev/null
+++ b/json/src/com/intellij/json/JsonLexer.java
@@ -0,0 +1,12 @@
+package com.intellij.json;
+
+import com.intellij.lexer.FlexAdapter;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonLexer extends FlexAdapter {
+ public JsonLexer() {
+ super(new _JsonLexer());
+ }
+}
diff --git a/json/src/com/intellij/json/JsonNamesValidator.java b/json/src/com/intellij/json/JsonNamesValidator.java
new file mode 100644
index 00000000..98a5bb34
--- /dev/null
+++ b/json/src/com/intellij/json/JsonNamesValidator.java
@@ -0,0 +1,38 @@
+/*
+ * @author max
+ */
+package com.intellij.json;
+
+import com.intellij.lang.refactoring.NamesValidator;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonNamesValidator implements NamesValidator {
+
+ private final JsonLexer myLexer = new JsonLexer();
+
+ @Override
+ public synchronized boolean isKeyword(@NotNull String name, Project project) {
+ myLexer.start(name);
+ return JsonParserDefinition.JSON_KEYWORDS.contains( myLexer.getTokenType() ) && myLexer.getTokenEnd() == name.length();
+ }
+ @Override
+ public synchronized boolean isIdentifier(@NotNull String name, final Project project) {
+ if (!StringUtil.startsWithChar(name,'\'') && !StringUtil.startsWithChar(name,'\"')) {
+ name = "\"" + name;
+ }
+
+ if (!StringUtil.endsWithChar(name,'"') && !StringUtil.endsWithChar(name,'\"')) {
+ name += "\"";
+ }
+
+ myLexer.start(name);
+ IElementType type = myLexer.getTokenType();
+
+ return myLexer.getTokenEnd() == name.length() && (type == JsonElementTypes.DOUBLE_QUOTED_STRING ||
+ type == JsonElementTypes.SINGLE_QUOTED_STRING);
+ }
+
+}
diff --git a/json/src/com/intellij/json/JsonParserDefinition.java b/json/src/com/intellij/json/JsonParserDefinition.java
new file mode 100644
index 00000000..06ecf6cd
--- /dev/null
+++ b/json/src/com/intellij/json/JsonParserDefinition.java
@@ -0,0 +1,83 @@
+package com.intellij.json;
+
+import com.intellij.json.psi.impl.JsonFileImpl;
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.ParserDefinition;
+import com.intellij.lang.PsiParser;
+import com.intellij.lexer.Lexer;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.tree.IFileElementType;
+import com.intellij.psi.tree.TokenSet;
+import org.jetbrains.annotations.NotNull;
+
+import static com.intellij.json.JsonElementTypes.*;
+
+public class JsonParserDefinition implements ParserDefinition {
+ public static final TokenSet WHITE_SPACES = TokenSet.WHITE_SPACE;
+ public static final TokenSet STRING_LITERALS = TokenSet.create(SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING);
+
+ public static final IFileElementType FILE = new IFileElementType(JsonLanguage.INSTANCE);
+
+ public static final TokenSet JSON_BRACES = TokenSet.create(L_CURLY, R_CURLY);
+ public static final TokenSet JSON_BRACKETS = TokenSet.create(L_BRACKET, R_BRACKET);
+ public static final TokenSet JSON_CONTAINERS = TokenSet.create(OBJECT, ARRAY);
+ public static final TokenSet JSON_BOOLEANS = TokenSet.create(TRUE, FALSE);
+ public static final TokenSet JSON_KEYWORDS = TokenSet.create(TRUE, FALSE, NULL);
+ public static final TokenSet JSON_LITERALS = TokenSet.create(STRING_LITERAL, NUMBER_LITERAL, NULL_LITERAL, TRUE, FALSE);
+ public static final TokenSet JSON_VALUES = TokenSet.orSet(JSON_CONTAINERS, JSON_LITERALS);
+ public static final TokenSet JSON_COMMENTARIES = TokenSet.create(BLOCK_COMMENT, LINE_COMMENT);
+
+
+ @NotNull
+ @Override
+ public Lexer createLexer(Project project) {
+ return new JsonLexer();
+ }
+
+ @Override
+ public PsiParser createParser(Project project) {
+ return new JsonParser();
+ }
+
+ @Override
+ public IFileElementType getFileNodeType() {
+ return FILE;
+ }
+
+ @NotNull
+ @Override
+ public TokenSet getWhitespaceTokens() {
+ return WHITE_SPACES;
+ }
+
+ @NotNull
+ @Override
+ public TokenSet getCommentTokens() {
+ return JSON_COMMENTARIES;
+ }
+
+ @NotNull
+ @Override
+ public TokenSet getStringLiteralElements() {
+ return STRING_LITERALS;
+ }
+
+ @NotNull
+ @Override
+ public PsiElement createElement(ASTNode astNode) {
+ return Factory.createElement(astNode);
+ }
+
+ @Override
+ public PsiFile createFile(FileViewProvider fileViewProvider) {
+ return new JsonFileImpl(fileViewProvider, JsonLanguage.INSTANCE);
+ }
+
+ @Override
+ public SpaceRequirements spaceExistenceTypeBetweenTokens(ASTNode astNode, ASTNode astNode2) {
+ return SpaceRequirements.MAY;
+ }
+}
diff --git a/json/src/com/intellij/json/JsonQuoteHandler.java b/json/src/com/intellij/json/JsonQuoteHandler.java
new file mode 100644
index 00000000..d2ff4bcf
--- /dev/null
+++ b/json/src/com/intellij/json/JsonQuoteHandler.java
@@ -0,0 +1,40 @@
+package com.intellij.json;
+
+import com.intellij.codeInsight.editorActions.MultiCharQuoteHandler;
+import com.intellij.codeInsight.editorActions.SimpleTokenSetQuoteHandler;
+import com.intellij.json.editor.JsonTypedHandler;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.highlighter.HighlighterIterator;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.TokenType;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonQuoteHandler extends SimpleTokenSetQuoteHandler implements MultiCharQuoteHandler {
+ public JsonQuoteHandler() {
+ super(JsonParserDefinition.STRING_LITERALS);
+ }
+
+ @Nullable
+ @Override
+ public CharSequence getClosingQuote(@NotNull HighlighterIterator iterator, int offset) {
+ final IElementType tokenType = iterator.getTokenType();
+ if (tokenType == TokenType.WHITE_SPACE) {
+ final int index = iterator.getStart() - 1;
+ if (index >= 0) {
+ return String.valueOf(iterator.getDocument().getCharsSequence().charAt(index));
+ }
+ }
+ return tokenType == JsonElementTypes.SINGLE_QUOTED_STRING ? "'" : "\"";
+ }
+
+ @Override
+ public void insertClosingQuote(@NotNull Editor editor, int offset, PsiFile file, @NotNull CharSequence closingQuote) {
+ editor.getDocument().insertString(offset, closingQuote);
+ JsonTypedHandler.processPairedBracesComma(closingQuote.charAt(0), editor, file);
+ }
+}
diff --git a/json/src/com/intellij/json/JsonSpellcheckerStrategy.java b/json/src/com/intellij/json/JsonSpellcheckerStrategy.java
new file mode 100644
index 00000000..1463baae
--- /dev/null
+++ b/json/src/com/intellij/json/JsonSpellcheckerStrategy.java
@@ -0,0 +1,87 @@
+package com.intellij.json;
+
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiUtilCore;
+import com.intellij.spellchecker.inspections.PlainTextSplitter;
+import com.intellij.spellchecker.tokenizer.SpellcheckingStrategy;
+import com.intellij.spellchecker.tokenizer.TokenConsumer;
+import com.intellij.spellchecker.tokenizer.Tokenizer;
+import com.intellij.util.ThreeState;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonOriginalPsiWalker;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import com.jetbrains.jsonSchema.impl.JsonSchemaResolver;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVariantsTreeBuilder;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonSpellcheckerStrategy extends SpellcheckingStrategy {
+ private final Tokenizer<JsonStringLiteral> ourStringLiteralTokenizer = new Tokenizer<JsonStringLiteral>() {
+ @Override
+ public void tokenize(@NotNull JsonStringLiteral element, TokenConsumer consumer) {
+ final PlainTextSplitter textSplitter = PlainTextSplitter.getInstance();
+ if (element.textContains('\\')) {
+ final List<Pair<TextRange, String>> fragments = element.getTextFragments();
+ for (Pair<TextRange, String> fragment : fragments) {
+ final TextRange fragmentRange = fragment.getFirst();
+ final String escaped = fragment.getSecond();
+ // Fragment without escaping, also not a broken escape sequence or a unicode code point
+ if (escaped.length() == fragmentRange.getLength() && !escaped.startsWith("\\")) {
+ consumer.consumeToken(element, escaped, false, fragmentRange.getStartOffset(), TextRange.allOf(escaped), textSplitter);
+ }
+ }
+ }
+ else {
+ consumer.consumeToken(element, textSplitter);
+ }
+ }
+ };
+
+ private static boolean matchesNameFromSchema(@NotNull JsonStringLiteral element) {
+ final VirtualFile file = PsiUtilCore.getVirtualFile(element);
+ if (file == null) return false;
+
+ final JsonSchemaService service = JsonSchemaService.Impl.get(element.getProject());
+ if (!service.isApplicableToFile(file)) return false;
+ final JsonSchemaObject rootSchema = service.getSchemaObject(file);
+ if (rootSchema == null) return false;
+
+ String value = element.getValue();
+ if (StringUtil.isEmpty(value)) return false;
+
+ JsonOriginalPsiWalker walker = JsonLikePsiWalker.JSON_ORIGINAL_PSI_WALKER;
+ final PsiElement checkable = walker.goUpToCheckable(element);
+ if (checkable == null) return false;
+ final ThreeState isName = walker.isName(checkable);
+ final List<JsonSchemaVariantsTreeBuilder.Step> position = walker.findPosition(checkable, isName == ThreeState.NO);
+ if (position == null || position.isEmpty() && isName == ThreeState.NO) return false;
+
+ final Collection<JsonSchemaObject> schemas = new JsonSchemaResolver(rootSchema, false, position).resolve();
+ if (schemas.isEmpty()) return false;
+
+ return schemas.stream().anyMatch(s -> s.getProperties().keySet().contains(value)
+ || s.getMatchingPatternPropertySchema(value) != null);
+ }
+
+ @NotNull
+ @Override
+ public Tokenizer getTokenizer(PsiElement element) {
+ if (element instanceof JsonStringLiteral) {
+ return matchesNameFromSchema((JsonStringLiteral)element)
+ ? EMPTY_TOKENIZER
+ : ourStringLiteralTokenizer;
+ }
+ return super.getTokenizer(element);
+ }
+}
diff --git a/json/src/com/intellij/json/JsonTokenType.java b/json/src/com/intellij/json/JsonTokenType.java
new file mode 100644
index 00000000..a7752cd8
--- /dev/null
+++ b/json/src/com/intellij/json/JsonTokenType.java
@@ -0,0 +1,11 @@
+package com.intellij.json;
+
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonTokenType extends IElementType {
+ public JsonTokenType(@NotNull @NonNls String debugName) {
+ super(debugName, JsonLanguage.INSTANCE);
+ }
+}
diff --git a/json/src/com/intellij/json/JsonUtil.java b/json/src/com/intellij/json/JsonUtil.java
new file mode 100644
index 00000000..51f751b9
--- /dev/null
+++ b/json/src/com/intellij/json/JsonUtil.java
@@ -0,0 +1,94 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json;
+
+import com.intellij.json.psi.*;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.ObjectUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonUtil {
+ private JsonUtil() {
+ // empty
+ }
+
+ /**
+ * Clone of C# "as" operator.
+ * Checks if expression has correct type and casts it if it has. Returns null otherwise.
+ * It saves coder from "instanceof / cast" chains.
+ *
+ * Copied from PyCharm's {@code PyUtil}.
+ *
+ * @param expression expression to check
+ * @param cls class to cast
+ * @param <T> class to cast
+ * @return expression casted to appropriate type (if could be casted). Null otherwise.
+ */
+ @Nullable
+ @SuppressWarnings("unchecked")
+ public static <T> T as(@Nullable final Object expression, @NotNull final Class<T> cls) {
+ if (expression == null) {
+ return null;
+ }
+ if (cls.isAssignableFrom(expression.getClass())) {
+ return (T)expression;
+ }
+ return null;
+ }
+
+ @Nullable
+ public static <T extends JsonElement> T getPropertyValueOfType(@NotNull final JsonObject object, @NotNull final String name,
+ @NotNull final Class<T> clazz) {
+ final JsonProperty property = object.findProperty(name);
+ if (property == null) return null;
+ return ObjectUtils.tryCast(property.getValue(), clazz);
+ }
+
+ @Nullable
+ public static List<String> getChildAsStringList(@NotNull final JsonObject object, @NotNull final String name) {
+ final JsonArray array = getPropertyValueOfType(object, name, JsonArray.class);
+ if (array != null) return array.getValueList().stream().filter(value -> value instanceof JsonStringLiteral)
+ .map(value -> StringUtil.unquoteString(value.getText())).collect(Collectors.toList());
+ return null;
+ }
+
+ @Nullable
+ public static List<String> getChildAsSingleStringOrList(@NotNull final JsonObject object, @NotNull final String name) {
+ final List<String> list = getChildAsStringList(object, name);
+ if (list != null) return list;
+ final JsonStringLiteral literal = getPropertyValueOfType(object, name, JsonStringLiteral.class);
+ return literal == null ? null : Collections.singletonList(StringUtil.unquoteString(literal.getText()));
+ }
+
+ public static boolean isArrayElement(@NotNull PsiElement element) {
+ return element instanceof JsonValue && element.getParent() instanceof JsonArray;
+ }
+
+ public static int getArrayIndexOfItem(@NotNull PsiElement e) {
+ PsiElement parent = e.getParent();
+ if (!(parent instanceof JsonArray)) return -1;
+ List<JsonValue> elements = ((JsonArray)parent).getValueList();
+ for (int i = 0; i < elements.size(); i++) {
+ if (e == elements.get(i)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public static boolean isJsonFile(@NotNull VirtualFile file) {
+ FileType type = file.getFileType();
+ return type instanceof LanguageFileType && ((LanguageFileType)type).getLanguage() instanceof JsonLanguage;
+ }
+}
diff --git a/json/src/com/intellij/json/_JsonLexer.flex b/json/src/com/intellij/json/_JsonLexer.flex
new file mode 100644
index 00000000..e48e9b8d
--- /dev/null
+++ b/json/src/com/intellij/json/_JsonLexer.flex
@@ -0,0 +1,58 @@
+package com.intellij.json;
+
+import com.intellij.lexer.FlexLexer;
+import com.intellij.psi.tree.IElementType;
+
+import static com.intellij.psi.TokenType.BAD_CHARACTER;
+import static com.intellij.psi.TokenType.WHITE_SPACE;
+import static com.intellij.json.JsonElementTypes.*;
+
+%%
+
+%{
+ public _JsonLexer() {
+ this((java.io.Reader)null);
+ }
+%}
+
+%public
+%class _JsonLexer
+%implements FlexLexer
+%function advance
+%type IElementType
+%unicode
+
+EOL=\R
+WHITE_SPACE=\s+
+
+LINE_COMMENT="//".*
+BLOCK_COMMENT="/"\*([^*]|\*+[^*/])*(\*+"/")?
+DOUBLE_QUOTED_STRING=\"([^\\\"\r\n]|\\[^\r\n])*\"?
+SINGLE_QUOTED_STRING='([^\\'\r\n]|\\[^\r\n])*'?
+NUMBER=(-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]*)?)|Infinity|-Infinity|NaN
+IDENTIFIER=[[:jletterdigit:]~!()*\-."/"@\^<>=]+
+
+%%
+<YYINITIAL> {
+ {WHITE_SPACE} { return WHITE_SPACE; }
+
+ "{" { return L_CURLY; }
+ "}" { return R_CURLY; }
+ "[" { return L_BRACKET; }
+ "]" { return R_BRACKET; }
+ "," { return COMMA; }
+ ":" { return COLON; }
+ "true" { return TRUE; }
+ "false" { return FALSE; }
+ "null" { return NULL; }
+
+ {LINE_COMMENT} { return LINE_COMMENT; }
+ {BLOCK_COMMENT} { return BLOCK_COMMENT; }
+ {DOUBLE_QUOTED_STRING} { return DOUBLE_QUOTED_STRING; }
+ {SINGLE_QUOTED_STRING} { return SINGLE_QUOTED_STRING; }
+ {NUMBER} { return NUMBER; }
+ {IDENTIFIER} { return IDENTIFIER; }
+
+}
+
+[^] { return BAD_CHARACTER; }
diff --git a/json/src/com/intellij/json/breadcrumbs/JsonBreadcrumbsProvider.java b/json/src/com/intellij/json/breadcrumbs/JsonBreadcrumbsProvider.java
new file mode 100644
index 00000000..7ab178f8
--- /dev/null
+++ b/json/src/com/intellij/json/breadcrumbs/JsonBreadcrumbsProvider.java
@@ -0,0 +1,73 @@
+package com.intellij.json.breadcrumbs;
+
+import com.intellij.json.JsonBundle;
+import com.intellij.json.JsonLanguage;
+import com.intellij.json.JsonUtil;
+import com.intellij.json.navigation.JsonQualifiedNameKind;
+import com.intellij.json.navigation.JsonQualifiedNameProvider;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.lang.Language;
+import com.intellij.openapi.ide.CopyPasteManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.ui.breadcrumbs.BreadcrumbsProvider;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.impl.JsonSchemaDocumentationProvider;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.datatransfer.StringSelection;
+import java.awt.event.ActionEvent;
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonBreadcrumbsProvider implements BreadcrumbsProvider {
+ private static final Language[] LANGUAGES = new Language[]{JsonLanguage.INSTANCE};
+
+ @Override
+ public Language[] getLanguages() {
+ return LANGUAGES;
+ }
+
+ @Override
+ public boolean acceptElement(@NotNull PsiElement e) {
+ return e instanceof JsonProperty || JsonUtil.isArrayElement(e);
+ }
+
+ @NotNull
+ @Override
+ public String getElementInfo(@NotNull PsiElement e) {
+ if (e instanceof JsonProperty) {
+ return ((JsonProperty)e).getName();
+ }
+ else if (JsonUtil.isArrayElement(e)) {
+ int i = JsonUtil.getArrayIndexOfItem(e);
+ if (i != -1) return String.valueOf(i);
+ }
+ throw new AssertionError("Breadcrumbs can be extracted only from JsonProperty elements or JsonArray child items");
+ }
+
+ @Nullable
+ @Override
+ public String getElementTooltip(@NotNull PsiElement e) {
+ return JsonSchemaDocumentationProvider.findSchemaAndGenerateDoc(e, null, true, null);
+ }
+
+ @NotNull
+ @Override
+ public List<? extends Action> getContextActions(@NotNull PsiElement element) {
+ JsonQualifiedNameKind[] values = JsonQualifiedNameKind.values();
+ List<Action> actions = ContainerUtil.newArrayListWithCapacity(values.length);
+ for (JsonQualifiedNameKind kind: values) {
+ actions.add(new AbstractAction(JsonBundle.message("json.copy.to.clipboard", kind.toString())) {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ CopyPasteManager.getInstance().setContents(new StringSelection(JsonQualifiedNameProvider.generateQualifiedName(element, kind)));
+ }
+ });
+ }
+ return actions;
+ }
+}
diff --git a/json/src/com/intellij/json/codeinsight/JsonCompletionContributor.java b/json/src/com/intellij/json/codeinsight/JsonCompletionContributor.java
new file mode 100644
index 00000000..4d05bf51
--- /dev/null
+++ b/json/src/com/intellij/json/codeinsight/JsonCompletionContributor.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2000-2014 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.intellij.json.codeinsight;
+
+import com.intellij.codeInsight.completion.*;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.patterns.PsiElementPattern;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.ProcessingContext;
+import org.jetbrains.annotations.NotNull;
+
+import static com.intellij.patterns.PlatformPatterns.psiElement;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonCompletionContributor extends CompletionContributor {
+
+ private static final PsiElementPattern.Capture<PsiElement> AFTER_COLON_IN_PROPERTY = psiElement()
+ .afterLeaf(":").withSuperParent(2, JsonProperty.class)
+ .andNot(psiElement().withParent(JsonStringLiteral.class));
+
+ private static final PsiElementPattern.Capture<PsiElement> AFTER_COMMA_OR_BRACKET_IN_ARRAY = psiElement()
+ .afterLeaf("[", ",").withSuperParent(2, JsonArray.class)
+ .andNot(psiElement().withParent(JsonStringLiteral.class));
+
+ public JsonCompletionContributor() {
+ extend(CompletionType.BASIC, AFTER_COLON_IN_PROPERTY, MyKeywordsCompletionProvider.INSTANCE);
+ extend(CompletionType.BASIC, AFTER_COMMA_OR_BRACKET_IN_ARRAY, MyKeywordsCompletionProvider.INSTANCE);
+ }
+
+ private static class MyKeywordsCompletionProvider extends CompletionProvider<CompletionParameters> {
+ private static final MyKeywordsCompletionProvider INSTANCE = new MyKeywordsCompletionProvider();
+ private static final String[] KEYWORDS = new String[]{"null", "true", "false"};
+
+ @Override
+ protected void addCompletions(@NotNull CompletionParameters parameters,
+ @NotNull ProcessingContext context,
+ @NotNull CompletionResultSet result) {
+ for (String keyword : KEYWORDS) {
+ result.addElement(LookupElementBuilder.create(keyword).bold());
+ }
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/codeinsight/JsonDuplicatePropertyKeysInspection.java b/json/src/com/intellij/json/codeinsight/JsonDuplicatePropertyKeysInspection.java
new file mode 100644
index 00000000..c9747679
--- /dev/null
+++ b/json/src/com/intellij/json/codeinsight/JsonDuplicatePropertyKeysInspection.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2000-2015 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.intellij.json.codeinsight;
+
+import com.intellij.codeInspection.LocalInspectionTool;
+import com.intellij.codeInspection.LocalQuickFixBase;
+import com.intellij.codeInspection.ProblemDescriptor;
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.icons.AllIcons;
+import com.intellij.json.JsonBundle;
+import com.intellij.json.psi.JsonElementVisitor;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.ScrollType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.ui.popup.PopupStep;
+import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.SmartPointerManager;
+import com.intellij.psi.SmartPsiElementPointer;
+import com.intellij.psi.util.PsiEditorUtil;
+import com.intellij.util.containers.MultiMap;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonDuplicatePropertyKeysInspection extends LocalInspectionTool {
+ @Nls
+ @NotNull
+ @Override
+ public String getDisplayName() {
+ return JsonBundle.message("inspection.duplicate.keys.name");
+ }
+
+ @NotNull
+ @Override
+ public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
+ return new JsonElementVisitor() {
+ @Override
+ public void visitObject(@NotNull JsonObject o) {
+ final MultiMap<String, PsiElement> keys = new MultiMap<>();
+ for (JsonProperty property : o.getPropertyList()) {
+ keys.putValue(property.getName(), property.getNameElement());
+ }
+ for (Map.Entry<String, Collection<PsiElement>> entry : keys.entrySet()) {
+ final Collection<PsiElement> sameNamedKeys = entry.getValue();
+ final String entryKey = entry.getKey();
+ if (sameNamedKeys.size() > 1) {
+ for (PsiElement element : sameNamedKeys) {
+ holder.registerProblem(element, JsonBundle.message("inspection.duplicate.keys.msg.duplicate.keys", entryKey),
+ new NavigateToDuplicatesFix(sameNamedKeys, element, entryKey));
+ }
+ }
+ }
+ }
+ };
+ }
+
+ private static class NavigateToDuplicatesFix extends LocalQuickFixBase {
+ @NotNull private final Collection<SmartPsiElementPointer> mySameNamedKeys;
+ @NotNull private final SmartPsiElementPointer myElement;
+ @NotNull private final String myEntryKey;
+
+ private NavigateToDuplicatesFix(@NotNull Collection<PsiElement> sameNamedKeys, @NotNull PsiElement element, @NotNull String entryKey) {
+ super("Navigate to duplicates");
+ mySameNamedKeys = sameNamedKeys.stream().map(k -> SmartPointerManager.createPointer(k)).collect(Collectors.toList());
+ myElement = SmartPointerManager.createPointer(element);
+ myEntryKey = entryKey;
+ }
+
+ @Override
+ public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
+ final Editor editor =
+ PsiEditorUtil.Service.getInstance().findEditorByPsiElement(descriptor.getPsiElement());
+ if (editor == null) return;
+ applyFix(editor);
+ }
+
+
+ private void applyFix(@NotNull Editor editor) {
+ final PsiElement currentElement = myElement.getElement();
+ if (mySameNamedKeys.size() == 2) {
+ final Iterator<SmartPsiElementPointer> iterator = mySameNamedKeys.iterator();
+ final PsiElement next = iterator.next().getElement();
+ PsiElement toNavigate = next != currentElement ? next : iterator.next().getElement();
+ if (toNavigate == null) return;
+ navigateTo(editor, toNavigate);
+ }
+ else {
+ final List<PsiElement> allElements =
+ mySameNamedKeys.stream().map(k -> k.getElement()).filter(k -> k != currentElement).collect(Collectors.toList());
+ JBPopupFactory.getInstance().createListPopup(new BaseListPopupStep<PsiElement>("Duplicates of '" + myEntryKey + "'", allElements) {
+ @NotNull
+ @Override
+ public Icon getIconFor(PsiElement aValue) {
+ return AllIcons.Nodes.Property;
+ }
+
+ @NotNull
+ @Override
+ public String getTextFor(PsiElement value) {
+ return "'" + myEntryKey + "' at line #" + editor.getDocument().getLineNumber(value.getTextOffset());
+ }
+
+ @Override
+ public int getDefaultOptionIndex() {
+ return 0;
+ }
+
+ @Nullable
+ @Override
+ public PopupStep onChosen(PsiElement selectedValue, boolean finalChoice) {
+ navigateTo(editor, selectedValue);
+ return PopupStep.FINAL_CHOICE;
+ }
+
+ @Override
+ public boolean isSpeedSearchEnabled() {
+ return true;
+ }
+ }).showInBestPositionFor(editor);
+ }
+ }
+
+ private static void navigateTo(@NotNull Editor editor, @NotNull PsiElement toNavigate) {
+ editor.getCaretModel().moveToOffset(toNavigate.getTextOffset());
+ editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/codeinsight/JsonLiteralAnnotator.java b/json/src/com/intellij/json/codeinsight/JsonLiteralAnnotator.java
new file mode 100644
index 00000000..4ad034d8
--- /dev/null
+++ b/json/src/com/intellij/json/codeinsight/JsonLiteralAnnotator.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2000-2015 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.intellij.json.codeinsight;
+
+import com.intellij.json.JsonBundle;
+import com.intellij.json.highlighting.JsonSyntaxHighlighterFactory;
+import com.intellij.json.psi.JsonNumberLiteral;
+import com.intellij.json.psi.JsonPsiUtil;
+import com.intellij.json.psi.JsonReferenceExpression;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.lang.annotation.AnnotationHolder;
+import com.intellij.lang.annotation.Annotator;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonLiteralAnnotator implements Annotator {
+
+ private static class Holder {
+ private static final boolean DEBUG = ApplicationManager.getApplication().isUnitTestMode();
+ }
+
+ @Override
+ public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
+ JsonLiteralChecker[] extensions = JsonLiteralChecker.EP_NAME.getExtensions();
+ if (element instanceof JsonReferenceExpression) {
+ highlightPropertyKey(element, holder);
+ }
+ else if (element instanceof JsonStringLiteral) {
+ final JsonStringLiteral stringLiteral = (JsonStringLiteral)element;
+ final int elementOffset = element.getTextOffset();
+ highlightPropertyKey(element, holder);
+ final String text = JsonPsiUtil.getElementTextWithoutHostEscaping(element);
+ final int length = text.length();
+
+ // Check that string literal is closed properly
+ if (length <= 1 || text.charAt(0) != text.charAt(length - 1) || JsonPsiUtil.isEscapedChar(text, length - 1)) {
+ holder.createErrorAnnotation(element, JsonBundle.message("syntax.error.missing.closing.quote"));
+ }
+
+ // Check escapes
+ final List<Pair<TextRange, String>> fragments = stringLiteral.getTextFragments();
+ for (Pair<TextRange, String> fragment: fragments) {
+ for (JsonLiteralChecker checker: extensions) {
+ if (!checker.isApplicable(element)) continue;
+ Pair<TextRange, String> error = checker.getErrorForStringFragment(fragment, stringLiteral);
+ if (error != null) {
+ holder.createErrorAnnotation(error.getFirst().shiftRight(elementOffset), error.second);
+ }
+ }
+ }
+ }
+ else if (element instanceof JsonNumberLiteral) {
+ String text = null;
+ for (JsonLiteralChecker checker: extensions) {
+ if (!checker.isApplicable(element)) continue;
+ if (text == null) {
+ text = JsonPsiUtil.getElementTextWithoutHostEscaping(element);
+ }
+ String error = checker.getErrorForNumericLiteral(text);
+ if (error != null) {
+ holder.createErrorAnnotation(element, error);
+ }
+ }
+ }
+ }
+
+ private static void highlightPropertyKey(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
+ if (JsonPsiUtil.isPropertyKey(element)) {
+ holder.createInfoAnnotation(element, Holder.DEBUG ? "property key" : null).setTextAttributes(JsonSyntaxHighlighterFactory.JSON_PROPERTY_KEY);
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/codeinsight/JsonLiteralChecker.java b/json/src/com/intellij/json/codeinsight/JsonLiteralChecker.java
new file mode 100644
index 00000000..6b8fe130
--- /dev/null
+++ b/json/src/com/intellij/json/codeinsight/JsonLiteralChecker.java
@@ -0,0 +1,21 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.codeinsight;
+
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.Nullable;
+
+public interface JsonLiteralChecker {
+ ExtensionPointName<JsonLiteralChecker> EP_NAME = ExtensionPointName.create("com.intellij.json.jsonLiteralChecker");
+
+ @Nullable
+ String getErrorForNumericLiteral(String literalText);
+
+ @Nullable
+ Pair<TextRange, String> getErrorForStringFragment(Pair<TextRange, String> fragmentText, JsonStringLiteral stringLiteral);
+
+ boolean isApplicable(PsiElement element);
+}
diff --git a/json/src/com/intellij/json/codeinsight/JsonStandardComplianceInspection.java b/json/src/com/intellij/json/codeinsight/JsonStandardComplianceInspection.java
new file mode 100644
index 00000000..564c2d85
--- /dev/null
+++ b/json/src/com/intellij/json/codeinsight/JsonStandardComplianceInspection.java
@@ -0,0 +1,234 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.codeinsight;
+
+import com.intellij.codeHighlighting.HighlightDisplayLevel;
+import com.intellij.codeInspection.LocalInspectionTool;
+import com.intellij.codeInspection.LocalQuickFix;
+import com.intellij.codeInspection.ProblemDescriptor;
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.codeInspection.ui.MultipleCheckboxOptionsPanel;
+import com.intellij.json.JsonBundle;
+import com.intellij.json.JsonDialectUtil;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiComment;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+/**
+ * Compliance checks include
+ * <ul>
+ * <li>Usage of line and block commentaries</li>
+ * <li>Usage of single quoted strings</li>
+ * <li>Usage of identifiers (unqouted words)</li>
+ * <li>Not double quoted string literal is used as property key</li>
+ * <li>Multiple top-level values</li>
+ * </ul>
+ *
+ * @author Mikhail Golubev
+ */
+public class JsonStandardComplianceInspection extends LocalInspectionTool {
+ private static final Logger LOG = Logger.getInstance(JsonStandardComplianceInspection.class);
+
+ public boolean myWarnAboutComments = true;
+ public boolean myWarnAboutNanInfinity = true;
+ public boolean myWarnAboutTrailingCommas = true;
+ public boolean myWarnAboutMultipleTopLevelValues = true;
+
+ @Override
+ @NotNull
+ public String getDisplayName() {
+ return JsonBundle.message("inspection.compliance.name");
+ }
+
+ @NotNull
+ @Override
+ public HighlightDisplayLevel getDefaultLevel() {
+ return HighlightDisplayLevel.ERROR;
+ }
+
+ @NotNull
+ @Override
+ public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
+ if (!JsonDialectUtil.isStandardJson(holder.getFile())) return PsiElementVisitor.EMPTY_VISITOR;
+ return new StandardJsonValidatingElementVisitor(holder);
+ }
+
+ @Nullable
+ private static PsiElement findTrailingComma(@NotNull JsonContainer container, @NotNull IElementType ending) {
+ final PsiElement lastChild = container.getLastChild();
+ if (lastChild.getNode().getElementType() != ending) {
+ return null;
+ }
+ final PsiElement beforeEnding = PsiTreeUtil.skipWhitespacesAndCommentsBackward(lastChild);
+ if (beforeEnding != null && beforeEnding.getNode().getElementType() == JsonElementTypes.COMMA) {
+ return beforeEnding;
+ }
+ return null;
+ }
+
+
+ @Override
+ public JComponent createOptionsPanel() {
+ final MultipleCheckboxOptionsPanel optionsPanel = new MultipleCheckboxOptionsPanel(this);
+ optionsPanel.addCheckbox(JsonBundle.message("inspection.compliance.option.comments"), "myWarnAboutComments");
+ optionsPanel.addCheckbox(JsonBundle.message("inspection.compliance.option.multiple.top.level.values"), "myWarnAboutMultipleTopLevelValues");
+ optionsPanel.addCheckbox(JsonBundle.message("inspection.compliance.option.trailing.comma"), "myWarnAboutTrailingCommas");
+ optionsPanel.addCheckbox(JsonBundle.message("inspection.compliance.option.nan.infinity"), "myWarnAboutNanInfinity");
+ return optionsPanel;
+ }
+
+ private static class AddDoubleQuotesFix implements LocalQuickFix {
+ @NotNull
+ @Override
+ public String getFamilyName() {
+ return JsonBundle.message("quickfix.add.double.quotes.desc");
+ }
+
+ @Override
+ public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
+ final PsiElement element = descriptor.getPsiElement();
+ final String rawText = element.getText();
+ if (element instanceof JsonLiteral || element instanceof JsonReferenceExpression) {
+ String content = JsonPsiUtil.stripQuotes(rawText);
+ if (element instanceof JsonStringLiteral && rawText.startsWith("'")) {
+ content = escapeSingleQuotedStringContent(content);
+ }
+ final PsiElement replacement = new JsonElementGenerator(project).createValue("\"" + content + "\"");
+ CodeStyleManager.getInstance(project).performActionWithFormatterDisabled((Runnable)() -> element.replace(replacement));
+ }
+ else {
+ LOG.error("Quick fix was applied to unexpected element", rawText, element.getParent().getText());
+ }
+ }
+
+ @NotNull
+ private static String escapeSingleQuotedStringContent(@NotNull String content) {
+ final StringBuilder result = new StringBuilder();
+ boolean nextCharEscaped = false;
+ for (int i = 0; i < content.length(); i++) {
+ final char c = content.charAt(i);
+ if ((nextCharEscaped && c != '\'') || (!nextCharEscaped && c == '"')) {
+ result.append('\\');
+ }
+ if (c != '\\' || nextCharEscaped) {
+ result.append(c);
+ nextCharEscaped = false;
+ }
+ else {
+ nextCharEscaped = true;
+ }
+ }
+ if (nextCharEscaped) {
+ result.append('\\');
+ }
+ return result.toString();
+ }
+ }
+
+ protected class StandardJsonValidatingElementVisitor extends JsonElementVisitor {
+ private final ProblemsHolder myHolder;
+
+ public StandardJsonValidatingElementVisitor(ProblemsHolder holder) {myHolder = holder;}
+
+ protected boolean allowComments() { return false; }
+ protected boolean allowSingleQuotes() { return false; }
+ protected boolean allowIdentifierPropertyNames() { return false; }
+ protected boolean allowTrailingCommas() { return false; }
+
+ protected boolean isValidPropertyName(@NotNull PsiElement literal) {
+ return literal instanceof JsonLiteral && JsonPsiUtil.getElementTextWithoutHostEscaping(literal).startsWith("\"");
+ }
+
+ @Override
+ public void visitComment(PsiComment comment) {
+ if (!allowComments() && myWarnAboutComments) {
+ if (JsonStandardComplianceProvider.shouldWarnAboutComment(comment)) {
+ myHolder.registerProblem(comment, JsonBundle.message("inspection.compliance.msg.comments"));
+ }
+ }
+ }
+
+ @Override
+ public void visitStringLiteral(@NotNull JsonStringLiteral stringLiteral) {
+ if (!allowSingleQuotes() && JsonPsiUtil.getElementTextWithoutHostEscaping(stringLiteral).startsWith("'")) {
+ myHolder.registerProblem(stringLiteral, JsonBundle.message("inspection.compliance.msg.single.quoted.strings"),
+ new AddDoubleQuotesFix());
+ }
+ // May be illegal property key as well
+ super.visitStringLiteral(stringLiteral);
+ }
+
+ @Override
+ public void visitLiteral(@NotNull JsonLiteral literal) {
+ if (JsonPsiUtil.isPropertyKey(literal) && !isValidPropertyName(literal)) {
+ myHolder.registerProblem(literal, JsonBundle.message("inspection.compliance.msg.illegal.property.key"), new AddDoubleQuotesFix());
+ }
+
+ // for standard JSON, the inspection for NaN, Infinity and -Infinity is now configurable
+ if (!allowNanInfinity() && literal instanceof JsonNumberLiteral && myWarnAboutNanInfinity) {
+ final String text = JsonPsiUtil.getElementTextWithoutHostEscaping(literal);
+ if (StandardJsonLiteralChecker.INF.equals(text) ||
+ StandardJsonLiteralChecker.MINUS_INF.equals(text) ||
+ StandardJsonLiteralChecker.NAN.equals(text)) {
+ myHolder.registerProblem(literal, JsonBundle.message("syntax.error.illegal.floating.point.literal"));
+ }
+ }
+ super.visitLiteral(literal);
+ }
+
+ protected boolean allowNanInfinity() {
+ return false;
+ }
+
+ @Override
+ public void visitReferenceExpression(@NotNull JsonReferenceExpression reference) {
+ if (!allowIdentifierPropertyNames() || !JsonPsiUtil.isPropertyKey(reference) || !isValidPropertyName(reference)) {
+ myHolder.registerProblem(reference, JsonBundle.message("inspection.compliance.msg.bad.token"), new AddDoubleQuotesFix());
+ }
+ // May be illegal property key as well
+ super.visitReferenceExpression(reference);
+ }
+
+ @Override
+ public void visitArray(@NotNull JsonArray array) {
+ if (myWarnAboutTrailingCommas && !allowTrailingCommas()) {
+ final PsiElement trailingComma = findTrailingComma(array, JsonElementTypes.R_BRACKET);
+ if (trailingComma != null) {
+ myHolder.registerProblem(trailingComma, JsonBundle.message("inspection.compliance.msg.trailing.comma"));
+ }
+ }
+ super.visitArray(array);
+ }
+
+ @Override
+ public void visitObject(@NotNull JsonObject object) {
+ if (myWarnAboutTrailingCommas && !allowTrailingCommas()) {
+ final PsiElement trailingComma = findTrailingComma(object, JsonElementTypes.R_CURLY);
+ if (trailingComma != null) {
+ myHolder.registerProblem(trailingComma, JsonBundle.message("inspection.compliance.msg.trailing.comma"));
+ }
+ }
+ super.visitObject(object);
+ }
+
+ @Override
+ public void visitValue(@NotNull JsonValue value) {
+ if (value.getContainingFile() instanceof JsonFile) {
+ final JsonFile jsonFile = (JsonFile)value.getContainingFile();
+ if (myWarnAboutMultipleTopLevelValues && value.getParent() == jsonFile && value != jsonFile.getTopLevelValue()) {
+ myHolder.registerProblem(value, JsonBundle.message("inspection.compliance.msg.multiple.top.level.values"));
+ }
+ }
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/codeinsight/JsonStandardComplianceProvider.java b/json/src/com/intellij/json/codeinsight/JsonStandardComplianceProvider.java
new file mode 100644
index 00000000..996cc82a
--- /dev/null
+++ b/json/src/com/intellij/json/codeinsight/JsonStandardComplianceProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2000-2014 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.intellij.json.codeinsight;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.psi.PsiComment;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Allows to configure a compliance level for JSON.
+ * For example, some tools ignore comments in JSON silently when parsing, so there is no need to warn users about it.
+ */
+public abstract class JsonStandardComplianceProvider {
+ public static final ExtensionPointName<JsonStandardComplianceProvider> EP_NAME =
+ ExtensionPointName.create("com.intellij.json.jsonStandardComplianceProvider");
+
+ public abstract boolean isCommentAllowed(@NotNull PsiComment comment);
+
+ public static boolean shouldWarnAboutComment(@NotNull PsiComment comment) {
+ JsonStandardComplianceProvider[] providers = EP_NAME.getExtensions();
+ if (providers.length == 0) {
+ return true;
+ }
+ for (JsonStandardComplianceProvider provider : providers) {
+ if (provider.isCommentAllowed(comment)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/json/src/com/intellij/json/codeinsight/JsonStringPropertyInsertHandler.java b/json/src/com/intellij/json/codeinsight/JsonStringPropertyInsertHandler.java
new file mode 100644
index 00000000..ebb20bf5
--- /dev/null
+++ b/json/src/com/intellij/json/codeinsight/JsonStringPropertyInsertHandler.java
@@ -0,0 +1,84 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.codeinsight;
+
+import com.intellij.codeInsight.AutoPopupController;
+import com.intellij.codeInsight.completion.InsertHandler;
+import com.intellij.codeInsight.completion.InsertionContext;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.ObjectUtils;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonStringPropertyInsertHandler implements InsertHandler<LookupElement> {
+
+ private final String myNewValue;
+
+ public JsonStringPropertyInsertHandler(@NotNull String newValue) {
+ myNewValue = newValue;
+ }
+
+ @Override
+ public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) {
+ PsiElement element = context.getFile().findElementAt(context.getStartOffset());
+ JsonStringLiteral literal = PsiTreeUtil.getParentOfType(element, JsonStringLiteral.class, false);
+ if (literal == null) return;
+ JsonProperty property = ObjectUtils.tryCast(literal.getParent(), JsonProperty.class);
+ if (property == null) return;
+ final TextRange toDelete;
+ String textToInsert = "";
+ TextRange literalRange = literal.getTextRange();
+ if (literal.getValue().equals(myNewValue)) {
+ toDelete = new TextRange(literalRange.getEndOffset(), literalRange.getEndOffset());
+ }
+ else {
+ toDelete = literalRange;
+ textToInsert = StringUtil.wrapWithDoubleQuote(myNewValue);
+ }
+ int newCaretOffset = literalRange.getStartOffset() + 1 + myNewValue.length();
+ boolean showAutoPopup = false;
+ if (property.getNameElement().equals(literal)) {
+ if (property.getValue() == null) {
+ textToInsert += ":\"\"";
+ newCaretOffset += 3; // "package<caret offset>":"<new caret offset>"
+ if (needCommaAfter(property)) {
+ textToInsert += ",";
+ }
+ showAutoPopup = true;
+ }
+ }
+ context.getDocument().replaceString(toDelete.getStartOffset(), toDelete.getEndOffset(), textToInsert);
+ context.getEditor().getCaretModel().moveToOffset(newCaretOffset);
+ reformat(context, toDelete.getStartOffset(), toDelete.getStartOffset() + textToInsert.length());
+ if (showAutoPopup) {
+ AutoPopupController.getInstance(context.getProject()).autoPopupMemberLookup(context.getEditor(), null);
+ }
+ }
+
+ private static boolean needCommaAfter(@NotNull JsonProperty property) {
+ PsiElement element = property.getNextSibling();
+ while (element != null) {
+ if (element instanceof JsonProperty) {
+ return true;
+ }
+ if (element.getNode().getElementType() == JsonElementTypes.COMMA) {
+ return false;
+ }
+ element = element.getNextSibling();
+ }
+ return false;
+ }
+
+ private static void reformat(@NotNull InsertionContext context, int startOffset, int endOffset) {
+ PsiDocumentManager.getInstance(context.getProject()).commitDocument(context.getDocument());
+ CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(context.getProject());
+ codeStyleManager.reformatText(context.getFile(), startOffset, endOffset);
+ }
+}
diff --git a/json/src/com/intellij/json/codeinsight/StandardJsonLiteralChecker.java b/json/src/com/intellij/json/codeinsight/StandardJsonLiteralChecker.java
new file mode 100644
index 00000000..06b87816
--- /dev/null
+++ b/json/src/com/intellij/json/codeinsight/StandardJsonLiteralChecker.java
@@ -0,0 +1,73 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.codeinsight;
+
+import com.intellij.json.JsonBundle;
+import com.intellij.json.JsonDialectUtil;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.regex.Pattern;
+
+public class StandardJsonLiteralChecker implements JsonLiteralChecker {
+ public static final Pattern VALID_ESCAPE = Pattern.compile("\\\\([\"\\\\/bfnrt]|u[0-9a-fA-F]{4})");
+ private static final Pattern VALID_NUMBER_LITERAL = Pattern.compile("-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?");
+ public static final String INF = "Infinity";
+ public static final String MINUS_INF = "-Infinity";
+ public static final String NAN = "NaN";
+
+ @Nullable
+ @Override
+ public String getErrorForNumericLiteral(String literalText) {
+ if (!INF.equals(literalText) &&
+ !MINUS_INF.equals(literalText) &&
+ !NAN.equals(literalText) &&
+ !VALID_NUMBER_LITERAL.matcher(literalText).matches()) {
+ return JsonBundle.message("syntax.error.illegal.floating.point.literal");
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Pair<TextRange, String> getErrorForStringFragment(Pair<TextRange, String> fragment, JsonStringLiteral stringLiteral) {
+ if (fragment.getSecond().chars().anyMatch(c -> c <= '\u001F')) { // fragments are cached, string values - aren't; go inside only if we encountered a potentially 'wrong' char
+ final String text = stringLiteral.getText();
+ if (new TextRange(0, text.length()).contains(fragment.first)) {
+ final int startOffset = fragment.first.getStartOffset();
+ final String part = text.substring(startOffset, fragment.first.getEndOffset());
+ char[] array = part.toCharArray();
+ for (int i = 0; i < array.length; i++) {
+ char c = array[i];
+ if (c <= '\u001F') {
+ return Pair.create(new TextRange(startOffset + i, startOffset + i + 1),
+ JsonBundle
+ .message("syntax.error.control.char.in.string", "\\u" + Integer.toHexString(c | 0x10000).substring(1)));
+ }
+ }
+ }
+ }
+ final String error = getStringError(fragment.second);
+ return error == null ? null : Pair.create(fragment.first, error);
+ }
+
+ @Nullable
+ public static String getStringError(String fragmentText) {
+ if (fragmentText.startsWith("\\") && fragmentText.length() > 1 && !VALID_ESCAPE.matcher(fragmentText).matches()) {
+ if (fragmentText.startsWith("\\u")) {
+ return JsonBundle.message("syntax.error.illegal.unicode.escape.sequence");
+ }
+ else {
+ return JsonBundle.message("syntax.error.illegal.escape.sequence");
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isApplicable(PsiElement element) {
+ return JsonDialectUtil.isStandardJson(element);
+ }
+}
diff --git a/json/src/com/intellij/json/editor/JsonCommenter.java b/json/src/com/intellij/json/editor/JsonCommenter.java
new file mode 100644
index 00000000..699f7006
--- /dev/null
+++ b/json/src/com/intellij/json/editor/JsonCommenter.java
@@ -0,0 +1,41 @@
+package com.intellij.json.editor;
+
+import com.intellij.lang.Commenter;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * JSON standard (RFC 4627) doesn't allow comments in documents, but they are added for compatibility with legacy JSON integration.
+ *
+ * @author Mikhail Golubev
+ */
+public class JsonCommenter implements Commenter {
+ @Nullable
+ @Override
+ public String getLineCommentPrefix() {
+ return "//";
+ }
+
+ @Nullable
+ @Override
+ public String getBlockCommentPrefix() {
+ return "/*";
+ }
+
+ @Nullable
+ @Override
+ public String getBlockCommentSuffix() {
+ return "*/";
+ }
+
+ @Nullable
+ @Override
+ public String getCommentedBlockCommentPrefix() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getCommentedBlockCommentSuffix() {
+ return null;
+ }
+}
diff --git a/json/src/com/intellij/json/editor/JsonCopyPastePostProcessor.java b/json/src/com/intellij/json/editor/JsonCopyPastePostProcessor.java
new file mode 100644
index 00000000..feb7068b
--- /dev/null
+++ b/json/src/com/intellij/json/editor/JsonCopyPastePostProcessor.java
@@ -0,0 +1,169 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.editor;
+
+import com.intellij.codeInsight.editorActions.CopyPastePostProcessor;
+import com.intellij.codeInsight.editorActions.TextBlockTransferableData;
+import com.intellij.ide.scratch.ScratchFileType;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.JsonFileType;
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.RangeMarker;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.*;
+import com.intellij.psi.impl.source.tree.LeafPsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.util.Collections;
+import java.util.List;
+
+public class JsonCopyPastePostProcessor extends CopyPastePostProcessor<TextBlockTransferableData> {
+ static final List<TextBlockTransferableData> DATA_LIST = Collections.singletonList(new DumbData());
+ static class DumbData implements TextBlockTransferableData {
+ private static final DataFlavor DATA_FLAVOR = new DataFlavor(JsonCopyPastePostProcessor.class, "class: JsonCopyPastePostProcessor");
+ @Override
+ public DataFlavor getFlavor() {
+ return DATA_FLAVOR;
+ }
+
+ @Override
+ public int getOffsetCount() {
+ return 0;
+ }
+
+ @Override
+ public int getOffsets(int[] offsets, int index) {
+ return index;
+ }
+
+ @Override
+ public int setOffsets(int[] offsets, int index) {
+ return index;
+ }
+ }
+
+ @NotNull
+ @Override
+ public List<TextBlockTransferableData> collectTransferableData(PsiFile file, Editor editor, int[] startOffsets, int[] endOffsets) {
+ return ContainerUtil.emptyList();
+ }
+
+ @NotNull
+ @Override
+ public List<TextBlockTransferableData> extractTransferableData(Transferable content) {
+ // if this list is empty, processTransferableData won't be called
+ return DATA_LIST;
+ }
+
+ @Override
+ public void processTransferableData(Project project,
+ Editor editor,
+ RangeMarker bounds,
+ int caretOffset,
+ Ref<Boolean> indented,
+ List<TextBlockTransferableData> values) {
+ fixCommasOnPaste(project, editor, bounds);
+ }
+
+ private static void fixCommasOnPaste(@NotNull Project project, @NotNull Editor editor, @NotNull RangeMarker bounds) {
+ if (!JsonEditorOptions.getInstance().COMMA_ON_PASTE) return;
+
+ if (!isJsonEditor(project, editor)) return;
+
+ final PsiDocumentManager manager = PsiDocumentManager.getInstance(project);
+ manager.commitDocument(editor.getDocument());
+ final PsiFile psiFile = manager.getPsiFile(editor.getDocument());
+ if (psiFile == null) return;
+ fixTrailingComma(bounds, psiFile, manager);
+ fixLeadingComma(bounds, psiFile, manager);
+ }
+
+ private static boolean isJsonEditor(@NotNull Project project,
+ @NotNull Editor editor) {
+ final VirtualFile file = FileDocumentManager.getInstance().getFile(editor.getDocument());
+ if (file == null) return false;
+ final FileType fileType = file.getFileType();
+ if (fileType instanceof JsonFileType) return true;
+ if (!(fileType instanceof ScratchFileType)) return false;
+ return PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()) instanceof JsonFile;
+ }
+
+ private static void fixLeadingComma(@NotNull RangeMarker bounds, @NotNull PsiFile psiFile, @NotNull PsiDocumentManager manager) {
+ final PsiElement startElement = skipWhitespaces(psiFile.findElementAt(bounds.getStartOffset()));
+ PsiElement propertyOrArrayItem = startElement instanceof JsonProperty ? startElement : getParentPropertyOrArrayItem(startElement);
+
+ if (propertyOrArrayItem == null) return;
+
+ PsiElement prevSibling = PsiTreeUtil.skipWhitespacesBackward(propertyOrArrayItem);
+ if (prevSibling instanceof PsiErrorElement) {
+ final int offset = prevSibling.getTextRange().getEndOffset();
+ ApplicationManager.getApplication().runWriteAction(() -> bounds.getDocument().insertString(offset, ","));
+ manager.commitDocument(bounds.getDocument());
+ }
+ }
+
+ @Nullable
+ private static PsiElement getParentPropertyOrArrayItem(@Nullable PsiElement startElement) {
+ PsiElement propertyOrArrayItem = PsiTreeUtil.getParentOfType(startElement, JsonProperty.class, JsonArray.class);
+ if (propertyOrArrayItem instanceof JsonArray) {
+ for (JsonValue value : ((JsonArray)propertyOrArrayItem).getValueList()) {
+ if (PsiTreeUtil.isAncestor(value, startElement, false)) {
+ return value;
+ }
+ }
+ return null;
+ }
+ return propertyOrArrayItem;
+ }
+
+ private static void fixTrailingComma(@NotNull RangeMarker bounds, @NotNull PsiFile psiFile, @NotNull PsiDocumentManager manager) {
+ PsiElement endElement = skipWhitespaces(psiFile.findElementAt(bounds.getEndOffset() - 1));
+ if (endElement != null && endElement.getTextOffset() >= bounds.getEndOffset()) {
+ endElement = PsiTreeUtil.skipWhitespacesBackward(endElement);
+ }
+
+ if (endElement instanceof LeafPsiElement && ((LeafPsiElement)endElement).getElementType() == JsonElementTypes.COMMA) {
+ final PsiElement nextNext = skipWhitespaces(endElement.getNextSibling());
+ if (nextNext instanceof LeafPsiElement && (((LeafPsiElement)nextNext).getElementType() == JsonElementTypes.R_CURLY ||
+ ((LeafPsiElement)nextNext).getElementType() == JsonElementTypes.R_BRACKET)) {
+ PsiElement finalEndElement = endElement;
+ ApplicationManager.getApplication().runWriteAction(() -> finalEndElement.delete());
+ }
+ }
+ else {
+ final PsiElement property = getParentPropertyOrArrayItem(endElement);
+ if (endElement instanceof PsiErrorElement || property != null && skipWhitespaces(property.getNextSibling()) instanceof PsiErrorElement) {
+ PsiElement finalEndElement1 = endElement;
+ ApplicationManager.getApplication().runWriteAction(() -> bounds.getDocument().insertString(getOffset(property, finalEndElement1), ","));
+ manager.commitDocument(bounds.getDocument());
+ }
+ }
+ }
+
+ private static int getOffset(@Nullable PsiElement property, @Nullable PsiElement finalEndElement1) {
+ if (finalEndElement1 instanceof PsiErrorElement) return finalEndElement1.getTextOffset();
+ assert finalEndElement1 != null;
+ return property != null ? property.getTextRange().getEndOffset() : finalEndElement1.getTextOffset();
+ }
+
+ @Nullable
+ private static PsiElement skipWhitespaces(@Nullable PsiElement element) {
+ while (element instanceof PsiWhiteSpace) {
+ element = element.getNextSibling();
+ }
+ return element;
+ }
+}
diff --git a/json/src/com/intellij/json/editor/JsonCopyPasteProcessor.java b/json/src/com/intellij/json/editor/JsonCopyPasteProcessor.java
new file mode 100644
index 00000000..e85e7167
--- /dev/null
+++ b/json/src/com/intellij/json/editor/JsonCopyPasteProcessor.java
@@ -0,0 +1,72 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.editor;
+
+import com.intellij.codeInsight.editorActions.CopyPastePreProcessor;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.RawText;
+import com.intellij.openapi.editor.SelectionModel;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class JsonCopyPasteProcessor implements CopyPastePreProcessor {
+ @Nullable
+ @Override
+ public String preprocessOnCopy(PsiFile file, int[] startOffsets, int[] endOffsets, String text) {
+ if (!JsonEditorOptions.getInstance().ESCAPE_PASTED_TEXT) {
+ return null;
+ }
+ if (!file.isPhysical() || startOffsets.length > 1 || endOffsets.length > 1) {
+ return null;
+ }
+ final int selectionStart = startOffsets[0];
+ final int selectionEnd = endOffsets[0];
+ final JsonStringLiteral literalExpression = getSingleElementFromSelectionOrNull(file, selectionStart, selectionEnd);
+
+ if (literalExpression == null) {
+ return null;
+ }
+
+ return StringUtil.unescapeStringCharacters(StringUtil.replaceUnicodeEscapeSequences(text));
+ }
+
+ @Nullable
+ private static JsonStringLiteral getSingleElementFromSelectionOrNull(PsiFile file, int start, int end) {
+ final PsiElement element = file.findElementAt(start);
+ final JsonStringLiteral literalExpression = PsiTreeUtil.getParentOfType(element, JsonStringLiteral.class);
+ if (literalExpression == null) return null;
+ TextRange textRange = literalExpression.getTextRange();
+ if (start <= textRange.getStartOffset() || end >= textRange.getEndOffset()) return null;
+ String text = literalExpression.getText();
+ if (!text.startsWith("\"") || !text.endsWith("\"")) return null;
+ return literalExpression;
+ }
+
+ @NotNull
+ @Override
+ public String preprocessOnPaste(Project project, PsiFile file, Editor editor, String text, RawText rawText) {
+ if (!JsonEditorOptions.getInstance().ESCAPE_PASTED_TEXT) {
+ return text;
+ }
+ if (!file.isPhysical()) {
+ return text;
+ }
+
+ final SelectionModel selectionModel = editor.getSelectionModel();
+ final int selectionStart = selectionModel.getSelectionStart();
+ final int selectionEnd = selectionModel.getSelectionEnd();
+
+ final JsonStringLiteral literalExpression = getSingleElementFromSelectionOrNull(file, selectionStart, selectionEnd);
+ if (literalExpression == null) {
+ return text;
+ }
+
+ return StringUtil.escapeStringCharacters(text);
+ }
+}
diff --git a/json/src/com/intellij/json/editor/JsonEditorOptions.java b/json/src/com/intellij/json/editor/JsonEditorOptions.java
new file mode 100644
index 00000000..53726fc9
--- /dev/null
+++ b/json/src/com/intellij/json/editor/JsonEditorOptions.java
@@ -0,0 +1,38 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.editor;
+
+import com.intellij.openapi.components.PersistentStateComponent;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.components.State;
+import com.intellij.openapi.components.Storage;
+import com.intellij.util.xmlb.XmlSerializerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+@State(
+ name = "JsonEditorOptions",
+ storages = @Storage("editor.xml")
+)
+public class JsonEditorOptions implements PersistentStateComponent<JsonEditorOptions> {
+ public boolean COMMA_ON_ENTER = true;
+ public boolean COMMA_ON_MATCHING_BRACES = true;
+ public boolean COMMA_ON_PASTE = true;
+ public boolean AUTO_QUOTE_PROP_NAME = true;
+ public boolean AUTO_WHITESPACE_AFTER_COLON = true;
+ public boolean ESCAPE_PASTED_TEXT = true;
+
+ @Nullable
+ @Override
+ public JsonEditorOptions getState() {
+ return this;
+ }
+
+ @Override
+ public void loadState(@NotNull JsonEditorOptions state) {
+ XmlSerializerUtil.copyBean(state, this);
+ }
+
+ public static JsonEditorOptions getInstance() {
+ return ServiceManager.getService(JsonEditorOptions.class);
+ }
+}
diff --git a/json/src/com/intellij/json/editor/JsonEnterHandler.java b/json/src/com/intellij/json/editor/JsonEnterHandler.java
new file mode 100644
index 00000000..53a2a047
--- /dev/null
+++ b/json/src/com/intellij/json/editor/JsonEnterHandler.java
@@ -0,0 +1,147 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.editor;
+
+import com.intellij.codeInsight.editorActions.EnterHandler;
+import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegateAdapter;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.JsonLanguage;
+import com.intellij.json.psi.*;
+import com.intellij.lang.Language;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiErrorElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiWhiteSpace;
+import com.intellij.psi.impl.source.tree.LeafPsiElement;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.util.ObjectUtils;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonEnterHandler extends EnterHandlerDelegateAdapter {
+ @Override
+ public Result preprocessEnter(@NotNull PsiFile file,
+ @NotNull Editor editor,
+ @NotNull Ref<Integer> caretOffsetRef,
+ @NotNull Ref<Integer> caretAdvanceRef,
+ @NotNull DataContext dataContext,
+ EditorActionHandler originalHandler) {
+ if (!JsonEditorOptions.getInstance().COMMA_ON_ENTER) {
+ return Result.Continue;
+ }
+
+ Language language = EnterHandler.getLanguage(dataContext);
+ if (!(language instanceof JsonLanguage)) {
+ return Result.Continue;
+ }
+
+ int caretOffset = caretOffsetRef.get().intValue();
+ PsiElement psiAtOffset = file.findElementAt(caretOffset);
+
+ if (psiAtOffset == null) {
+ return Result.Continue;
+ }
+
+ if (psiAtOffset instanceof LeafPsiElement && handleComma(caretOffsetRef, psiAtOffset, editor)) {
+ return Result.Continue;
+ }
+
+ JsonValue literal = ObjectUtils.tryCast(psiAtOffset.getParent(), JsonValue.class);
+ if (literal != null && (!(literal instanceof JsonStringLiteral) || !((JsonLanguage)language).hasPermissiveStrings())) {
+ handleJsonValue(literal, editor, caretOffsetRef);
+ }
+
+ return Result.Continue;
+ }
+
+ private static boolean handleComma(@NotNull Ref<Integer> caretOffsetRef, @NotNull PsiElement psiAtOffset, @NotNull Editor editor) {
+ PsiElement nextSibling = psiAtOffset;
+ while (nextSibling instanceof PsiWhiteSpace) {
+ nextSibling = nextSibling.getNextSibling();
+ }
+
+ LeafPsiElement leafPsiElement = ObjectUtils.tryCast(nextSibling, LeafPsiElement.class);
+ IElementType elementType = leafPsiElement == null ? null : leafPsiElement.getElementType();
+ if (elementType == JsonElementTypes.COMMA || elementType == JsonElementTypes.R_CURLY) {
+ PsiElement prevSibling = nextSibling.getPrevSibling();
+ while (prevSibling instanceof PsiWhiteSpace) {
+ prevSibling = prevSibling.getPrevSibling();
+ }
+
+ if (prevSibling instanceof JsonProperty && ((JsonProperty)prevSibling).getValue() != null) {
+ int offset = elementType == JsonElementTypes.COMMA ? nextSibling.getTextRange().getEndOffset() : prevSibling.getTextRange().getEndOffset();
+ if (offset < editor.getDocument().getTextLength()) {
+ if (elementType == JsonElementTypes.R_CURLY) {
+ editor.getDocument().insertString(offset, ",");
+ offset++;
+ }
+ caretOffsetRef.set(offset);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ if (nextSibling instanceof JsonProperty) {
+ PsiElement prevSibling = nextSibling.getPrevSibling();
+ while (prevSibling instanceof PsiWhiteSpace || prevSibling instanceof PsiErrorElement) {
+ prevSibling = prevSibling.getPrevSibling();
+ }
+
+ if (prevSibling instanceof JsonProperty) {
+ int offset = prevSibling.getTextRange().getEndOffset();
+ if (offset < editor.getDocument().getTextLength()) {
+ editor.getDocument().insertString(offset, ",");
+ caretOffsetRef.set(offset + 1);
+ }
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static void handleJsonValue(@NotNull JsonValue literal, @NotNull Editor editor, @NotNull Ref<Integer> caretOffsetRef) {
+ PsiElement parent = literal.getParent();
+ if (!(parent instanceof JsonProperty) || ((JsonProperty)parent).getValue() != literal) {
+ return;
+ }
+
+ PsiElement nextSibling = parent.getNextSibling();
+ while (nextSibling instanceof PsiWhiteSpace || nextSibling instanceof PsiErrorElement) {
+ nextSibling = nextSibling.getNextSibling();
+ }
+
+ int offset = literal.getTextRange().getEndOffset();
+
+ if (literal instanceof JsonObject || literal instanceof JsonArray) {
+ if (nextSibling instanceof LeafPsiElement && ((LeafPsiElement)nextSibling).getElementType() == JsonElementTypes.COMMA
+ || !(nextSibling instanceof JsonProperty)) {
+ return;
+ }
+ Document document = editor.getDocument();
+ if (offset < document.getTextLength()) {
+ document.insertString(offset, ",");
+ }
+ return;
+ }
+
+ if (nextSibling instanceof LeafPsiElement && ((LeafPsiElement)nextSibling).getElementType() == JsonElementTypes.COMMA) {
+ offset = nextSibling.getTextRange().getEndOffset();
+ }
+ else {
+ Document document = editor.getDocument();
+ if (offset < document.getTextLength()) {
+ document.insertString(offset, ",");
+ }
+ offset++;
+ }
+
+ if (offset < editor.getDocument().getTextLength()) {
+ caretOffsetRef.set(offset);
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/editor/JsonSmartKeysConfigurable.java b/json/src/com/intellij/json/editor/JsonSmartKeysConfigurable.java
new file mode 100644
index 00000000..83ac5b2f
--- /dev/null
+++ b/json/src/com/intellij/json/editor/JsonSmartKeysConfigurable.java
@@ -0,0 +1,42 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.editor;
+
+import com.intellij.openapi.options.BeanConfigurable;
+import com.intellij.openapi.options.UnnamedConfigurable;
+import com.intellij.ui.IdeBorderFactory;
+
+import javax.swing.*;
+
+public class JsonSmartKeysConfigurable extends BeanConfigurable<JsonEditorOptions> implements UnnamedConfigurable {
+ public JsonSmartKeysConfigurable() {
+ super(JsonEditorOptions.getInstance());
+ JsonEditorOptions settings = getInstance();
+
+ checkBox("Insert missing comma on enter",
+ () -> settings.COMMA_ON_ENTER,
+ v -> settings.COMMA_ON_ENTER = v);
+ checkBox("Insert missing comma after matching braces and quotes",
+ () -> settings.COMMA_ON_MATCHING_BRACES,
+ v -> settings.COMMA_ON_MATCHING_BRACES = v);
+ checkBox("Automatically manage commas when pasting JSON fragments",
+ () -> settings.COMMA_ON_PASTE,
+ v -> settings.COMMA_ON_PASTE = v);
+ checkBox("Escape text on paste in string literals",
+ () -> settings.ESCAPE_PASTED_TEXT,
+ v -> settings.ESCAPE_PASTED_TEXT = v);
+ checkBox("Automatically add quotes to property names when typing ':'",
+ () -> settings.AUTO_QUOTE_PROP_NAME,
+ v -> settings.AUTO_QUOTE_PROP_NAME = v);
+ checkBox("Automatically add whitespace when typing ':' after property namess",
+ () -> settings.AUTO_WHITESPACE_AFTER_COLON,
+ v -> settings.AUTO_WHITESPACE_AFTER_COLON = v);
+ }
+
+ @Override
+ public JComponent createComponent() {
+ JComponent result = super.createComponent();
+ assert result != null;
+ result.setBorder(IdeBorderFactory.createTitledBorder("JSON"));
+ return result;
+ }
+}
diff --git a/json/src/com/intellij/json/editor/JsonTypedHandler.java b/json/src/com/intellij/json/editor/JsonTypedHandler.java
new file mode 100644
index 00000000..7214f238
--- /dev/null
+++ b/json/src/com/intellij/json/editor/JsonTypedHandler.java
@@ -0,0 +1,142 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.editor;
+
+import com.intellij.codeInsight.editorActions.TypedHandlerDelegate;
+import com.intellij.codeInsight.editorActions.smartEnter.SmartEnterProcessor;
+import com.intellij.json.JsonDialectUtil;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.psi.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.*;
+import com.intellij.psi.tree.TokenSet;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonTypedHandler extends TypedHandlerDelegate {
+
+ private boolean myWhitespaceAdded;
+
+ @NotNull
+ @Override
+ public Result charTyped(char c, @NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
+ if (file instanceof JsonFile) {
+ processPairedBracesComma(c, editor, file);
+ addWhiteSpaceAfterColonIfNeeded(c, editor, file);
+ removeRedundantWhitespaceIfAfterColon(c, editor, file);
+ }
+ return Result.CONTINUE;
+ }
+
+ private void removeRedundantWhitespaceIfAfterColon(char c, Editor editor, PsiFile file) {
+ if (!myWhitespaceAdded || c != ' ' || !JsonEditorOptions.getInstance().AUTO_WHITESPACE_AFTER_COLON) {
+ if (c != ':') {
+ myWhitespaceAdded = false;
+ }
+ return;
+ }
+ int offset = editor.getCaretModel().getOffset();
+ PsiDocumentManager.getInstance(file.getProject()).commitDocument(editor.getDocument());
+ final PsiElement element = file.findElementAt(offset);
+ if (element instanceof PsiWhiteSpace) {
+ editor.getDocument().deleteString(offset - 1, offset);
+ }
+ myWhitespaceAdded = false;
+ }
+
+ @NotNull
+ @Override
+ public Result beforeCharTyped(char c,
+ @NotNull Project project,
+ @NotNull Editor editor,
+ @NotNull PsiFile file,
+ @NotNull FileType fileType) {
+ if (file instanceof JsonFile) {
+ addPropertyNameQuotesIfNeeded(c, editor, file);
+ }
+ return Result.CONTINUE;
+ }
+
+ private void addWhiteSpaceAfterColonIfNeeded(char c,
+ @NotNull Editor editor,
+ @NotNull PsiFile file) {
+ if (c != ':' || !JsonEditorOptions.getInstance().AUTO_WHITESPACE_AFTER_COLON) {
+ if (c != ' ') {
+ myWhitespaceAdded = false;
+ }
+ return;
+ }
+ int offset = editor.getCaretModel().getOffset();
+ PsiDocumentManager.getInstance(file.getProject()).commitDocument(editor.getDocument());
+ PsiElement element = PsiTreeUtil.getParentOfType(PsiTreeUtil.skipWhitespacesBackward(file.findElementAt(offset)), JsonProperty.class, false);
+ if (element == null) {
+ myWhitespaceAdded = false;
+ return;
+ }
+ final ASTNode[] children = element.getNode().getChildren(TokenSet.create(JsonElementTypes.COLON));
+ if (children.length == 0) {
+ myWhitespaceAdded = false;
+ return;
+ }
+ final ASTNode colon = children[0];
+ final ASTNode next = colon.getTreeNext();
+ final String text = next.getText();
+ if (text.length() == 0 || !StringUtil.isEmptyOrSpaces(text) || StringUtil.isLineBreak(text.charAt(0))) {
+ final int insOffset = colon.getStartOffset() + 1;
+ editor.getDocument().insertString(insOffset, " ");
+ editor.getCaretModel().moveToOffset(insOffset + 1);
+ myWhitespaceAdded = true;
+ }
+ else {
+ myWhitespaceAdded = false;
+ }
+ }
+
+ private static void addPropertyNameQuotesIfNeeded(char c,
+ @NotNull Editor editor,
+ @NotNull PsiFile file) {
+ if (c != ':' || !JsonDialectUtil.isStandardJson(file) || !JsonEditorOptions.getInstance().AUTO_QUOTE_PROP_NAME) return;
+ int offset = editor.getCaretModel().getOffset();
+ PsiElement element = PsiTreeUtil.skipWhitespacesBackward(file.findElementAt(offset));
+ if (!(element instanceof JsonProperty)) return;
+ final JsonValue nameElement = ((JsonProperty)element).getNameElement();
+ if (nameElement instanceof JsonReferenceExpression) {
+ ((JsonProperty)element).setName(nameElement.getText());
+ PsiDocumentManager.getInstance(file.getProject()).doPostponedOperationsAndUnblockDocument(editor.getDocument());
+ }
+ }
+
+ public static void processPairedBracesComma(char c,
+ @NotNull Editor editor,
+ @NotNull PsiFile file) {
+ if (!JsonEditorOptions.getInstance().COMMA_ON_MATCHING_BRACES) return;
+ if (c != '[' && c != '{' && c != '"' && c != '\'') return;
+ SmartEnterProcessor.commitDocument(editor);
+ int offset = editor.getCaretModel().getOffset();
+ PsiElement element = file.findElementAt(offset);
+ if (element == null) return;
+ PsiElement parent = element.getParent();
+ if (c == '[' && parent instanceof JsonArray
+ || c == '{' && parent instanceof JsonObject
+ || (c == '"' || c == '\'') && parent instanceof JsonStringLiteral) {
+ if (shouldAddCommaInParentContainer((JsonValue)parent)) {
+ editor.getDocument().insertString(parent.getTextRange().getEndOffset(), ",");
+ }
+ }
+ }
+
+ private static boolean shouldAddCommaInParentContainer(@NotNull JsonValue item) {
+ PsiElement parent = item.getParent();
+ if (parent instanceof JsonArray || parent instanceof JsonProperty) {
+ PsiElement nextElement = PsiTreeUtil.skipWhitespacesForward(parent instanceof JsonProperty ? parent : item);
+ if (nextElement instanceof PsiErrorElement) {
+ PsiElement forward = PsiTreeUtil.skipWhitespacesForward(nextElement);
+ return parent instanceof JsonProperty ? forward instanceof JsonProperty : forward instanceof JsonValue;
+ }
+ }
+ return false;
+ }
+}
diff --git a/json/src/com/intellij/json/editor/folding/JsonFoldingBuilder.java b/json/src/com/intellij/json/editor/folding/JsonFoldingBuilder.java
new file mode 100644
index 00000000..9bfc3103
--- /dev/null
+++ b/json/src/com/intellij/json/editor/folding/JsonFoldingBuilder.java
@@ -0,0 +1,110 @@
+package com.intellij.json.editor.folding;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.psi.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.folding.FoldingBuilder;
+import com.intellij.lang.folding.FoldingDescriptor;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.util.Couple;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonFoldingBuilder implements FoldingBuilder, DumbAware {
+ @NotNull
+ @Override
+ public FoldingDescriptor[] buildFoldRegions(@NotNull ASTNode node, @NotNull Document document) {
+ final List<FoldingDescriptor> descriptors = new ArrayList<>();
+ collectDescriptorsRecursively(node, document, descriptors);
+ return descriptors.toArray(FoldingDescriptor.EMPTY);
+ }
+
+ private static void collectDescriptorsRecursively(@NotNull ASTNode node,
+ @NotNull Document document,
+ @NotNull List<FoldingDescriptor> descriptors) {
+ final IElementType type = node.getElementType();
+ if ((type == JsonElementTypes.OBJECT || type == JsonElementTypes.ARRAY) && spanMultipleLines(node, document)) {
+ descriptors.add(new FoldingDescriptor(node, node.getTextRange()));
+ }
+ else if (type == JsonElementTypes.BLOCK_COMMENT) {
+ descriptors.add(new FoldingDescriptor(node, node.getTextRange()));
+ }
+ else if (type == JsonElementTypes.LINE_COMMENT) {
+ final Couple<PsiElement> commentRange = expandLineCommentsRange(node.getPsi());
+ final int startOffset = commentRange.getFirst().getTextRange().getStartOffset();
+ final int endOffset = commentRange.getSecond().getTextRange().getEndOffset();
+ if (document.getLineNumber(startOffset) != document.getLineNumber(endOffset)) {
+ descriptors.add(new FoldingDescriptor(node, new TextRange(startOffset, endOffset)));
+ }
+ }
+
+ for (ASTNode child : node.getChildren(null)) {
+ collectDescriptorsRecursively(child, document, descriptors);
+ }
+ }
+
+ @Nullable
+ @Override
+ public String getPlaceholderText(@NotNull ASTNode node) {
+ final IElementType type = node.getElementType();
+ if (type == JsonElementTypes.OBJECT) {
+ final JsonObject object = node.getPsi(JsonObject.class);
+ final List<JsonProperty> properties = object.getPropertyList();
+ JsonProperty candidate = null;
+ for (JsonProperty property : properties) {
+ final String name = property.getName();
+ final JsonValue value = property.getValue();
+ if (value instanceof JsonLiteral) {
+ if ("id".equals(name) || "name".equals(name)) {
+ candidate = property;
+ break;
+ }
+ if (candidate == null) {
+ candidate = property;
+ }
+ }
+ }
+ if (candidate != null) {
+ return "{\"" + candidate.getName() + "\": " + candidate.getValue().getText() + "...}";
+ }
+ return "{...}";
+ }
+ else if (type == JsonElementTypes.ARRAY) {
+ return "[...]";
+ }
+ else if (type == JsonElementTypes.LINE_COMMENT) {
+ return "//...";
+ }
+ else if (type == JsonElementTypes.BLOCK_COMMENT) {
+ return "/*...*/";
+ }
+ return "...";
+ }
+
+ @Override
+ public boolean isCollapsedByDefault(@NotNull ASTNode node) {
+ return false;
+ }
+
+ @NotNull
+ public static Couple<PsiElement> expandLineCommentsRange(@NotNull PsiElement anchor) {
+ return Couple.of(JsonPsiUtil.findFurthestSiblingOfSameType(anchor, false), JsonPsiUtil.findFurthestSiblingOfSameType(anchor, true));
+ }
+
+ private static boolean spanMultipleLines(@NotNull ASTNode node, @NotNull Document document) {
+ final TextRange range = node.getTextRange();
+ int endOffset = range.getEndOffset();
+ return document.getLineNumber(range.getStartOffset())
+ < (endOffset < document.getTextLength() ? document.getLineNumber(endOffset) : document.getLineCount() - 1);
+ }
+}
diff --git a/json/src/com/intellij/json/editor/lineMover/JsonLineMover.java b/json/src/com/intellij/json/editor/lineMover/JsonLineMover.java
new file mode 100644
index 00000000..ff82d1c8
--- /dev/null
+++ b/json/src/com/intellij/json/editor/lineMover/JsonLineMover.java
@@ -0,0 +1,197 @@
+package com.intellij.json.editor.lineMover;
+
+import com.intellij.codeInsight.editorActions.moveUpDown.LineMover;
+import com.intellij.codeInsight.editorActions.moveUpDown.LineRange;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiComment;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiWhiteSpace;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class JsonLineMover extends LineMover {
+ private enum Direction {
+ Same,
+ Inside,
+ Outside
+ }
+
+ private Direction myDirection = Direction.Same;
+
+ @Override
+ public boolean checkAvailable(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
+ myDirection = Direction.Same;
+
+ if (!(file instanceof JsonFile) || !super.checkAvailable(editor, file, info, down)) {
+ return false;
+ }
+
+ Pair<PsiElement, PsiElement> movedElementRange = getElementRange(editor, file, info.toMove);
+ if (!isValidElementRange(movedElementRange)) {
+ return false;
+ }
+
+ // Tweak range to move if it's necessary
+ movedElementRange = expandCommentsInRange(movedElementRange);
+
+ PsiElement movedSecond = movedElementRange.getSecond();
+ PsiElement movedFirst = movedElementRange.getFirst();
+
+ info.toMove = new LineRange(movedFirst, movedSecond);
+
+ // Adjust destination range to prevent illegal offsets
+ final int lineCount = editor.getDocument().getLineCount();
+ if (down) {
+ info.toMove2 = new LineRange(info.toMove.endLine, Math.min(info.toMove.endLine + 1, lineCount));
+ }
+ else {
+ info.toMove2 = new LineRange(Math.max(info.toMove.startLine - 1, 0), info.toMove.startLine);
+ }
+
+ if (movedFirst instanceof PsiComment && movedSecond instanceof PsiComment) {
+ return true;
+ }
+
+ // Check whether additional comma is needed
+ final Pair<PsiElement, PsiElement> destElementRange = getElementRange(editor, file, info.toMove2);
+
+ if (destElementRange != null) {
+ PsiElement destFirst = destElementRange.getFirst();
+ PsiElement destSecond = destElementRange.getSecond();
+
+ if (destFirst == destSecond && !(destFirst instanceof JsonProperty) && !(destFirst instanceof JsonValue)) {
+ PsiElement parent = destFirst.getParent();
+ if (((JsonFile)parent.getContainingFile()).getTopLevelValue() == parent) {
+ info.prohibitMove();
+ return true;
+ }
+ }
+
+ PsiElement firstParent = destFirst.getParent();
+ PsiElement secondParent = destSecond.getParent();
+
+ JsonValue firstParentParent = PsiTreeUtil.getParentOfType(firstParent, JsonObject.class, JsonArray.class);
+ if (firstParentParent == secondParent) {
+ myDirection = down ? Direction.Outside : Direction.Inside;
+ }
+ JsonValue secondParentParent = PsiTreeUtil.getParentOfType(secondParent, JsonObject.class, JsonArray.class);
+ if (firstParent == secondParentParent) {
+ myDirection = down ? Direction.Inside : Direction.Outside;
+ }
+ }
+ return true;
+ }
+
+ @NotNull
+ private static Pair<PsiElement, PsiElement> expandCommentsInRange(@NotNull Pair<PsiElement, PsiElement> range) {
+ final PsiElement upper = JsonPsiUtil.findFurthestSiblingOfSameType(range.getFirst(), false);
+ final PsiElement lower = JsonPsiUtil.findFurthestSiblingOfSameType(range.getSecond(), true);
+ return Pair.create(upper, lower);
+ }
+
+ @Override
+ public void beforeMove(@NotNull Editor editor, @NotNull MoveInfo info, boolean down) {
+
+ }
+
+ @Override
+ public void afterMove(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
+ int diff = (info.toMove.endLine - info.toMove.startLine) - (info.toMove2.endLine - info.toMove2.startLine);
+ switch (myDirection) {
+ case Same:
+ addCommaIfNeeded(editor.getDocument(), down ? info.toMove.endLine - 1 - diff : info.toMove2.endLine - 1 + diff);
+ trimCommaIfNeeded(editor.getDocument(), file, down ? info.toMove.endLine : info.toMove2.endLine + diff);
+ break;
+ case Inside:
+ if (!down) {
+ addCommaIfNeeded(editor.getDocument(), info.toMove2.startLine - 1);
+ }
+ trimCommaIfNeeded(editor.getDocument(), file, down ? info.toMove.startLine : info.toMove2.startLine);
+ trimCommaIfNeeded(editor.getDocument(), file, down ? info.toMove.endLine : info.toMove2.endLine + diff);
+ break;
+ case Outside:
+ addCommaIfNeeded(editor.getDocument(), down ? info.toMove.startLine : info.toMove2.startLine);
+ trimCommaIfNeeded(editor.getDocument(), file, down ? info.toMove.endLine : info.toMove2.endLine + diff);
+ if (down) {
+ trimCommaIfNeeded(editor.getDocument(), file, info.toMove.startLine - 1);
+ addCommaIfNeeded(editor.getDocument(), info.toMove.endLine);
+ trimCommaIfNeeded(editor.getDocument(), file, info.toMove.endLine);
+ }
+ break;
+ }
+ }
+
+ private static int getForwardLineNumber(Document document, PsiElement element) {
+ while (element instanceof PsiWhiteSpace || element instanceof PsiComment) {
+ element = element.getNextSibling();
+ }
+ if (element == null) return -1;
+
+ TextRange range = element.getTextRange();
+ return document.getLineNumber(range.getEndOffset());
+ }
+
+ private static int getBackwardLineNumber(Document document, PsiElement element) {
+ while (element instanceof PsiWhiteSpace || element instanceof PsiComment) {
+ element = element.getPrevSibling();
+ }
+ if (element == null) return -1;
+
+ TextRange range = element.getTextRange();
+ return document.getLineNumber(range.getEndOffset());
+ }
+
+ private static void trimCommaIfNeeded(Document document, PsiFile file, int line) {
+ int offset = document.getLineEndOffset(line);
+ if (doTrimComma(document, offset + 1, offset)) return;
+
+ PsiElement element = file.findElementAt(offset - 1);
+ int forward = getForwardLineNumber(document, element);
+ int backward = getBackwardLineNumber(document, element);
+ if (forward < 0 || backward < 0) return;
+ doTrimComma(document, document.getLineEndOffset(forward) - 1, document.getLineEndOffset(backward));
+ }
+
+ private static boolean doTrimComma(Document document, int forwardOffset, int backwardOffset) {
+ CharSequence charSequence = document.getCharsSequence();
+ if (backwardOffset <= 0) return true;
+ if (charSequence.charAt(backwardOffset - 1) == ',') {
+ int offsetAfter = skipWhitespaces(charSequence, forwardOffset);
+ if (offsetAfter >= charSequence.length()) return true;
+ char ch = charSequence.charAt(offsetAfter);
+
+ if (ch == ']' || ch == '}') {
+ document.deleteString(backwardOffset - 1, backwardOffset);
+ }
+ if (ch != '/') return true;
+ }
+ return false;
+ }
+
+ private static int skipWhitespaces(CharSequence charSequence, int offset2) {
+ while (offset2 < charSequence.length() && Character.isWhitespace(charSequence.charAt(offset2))) {
+ offset2++;
+ }
+ return offset2;
+ }
+
+ private static void addCommaIfNeeded(Document document, int line) {
+ int offset = document.getLineEndOffset(line);
+ if (offset > 0 && document.getCharsSequence().charAt(offset - 1) != ',') {
+ document.insertString(offset, ",");
+ }
+ }
+
+ private static boolean isValidElementRange(@Nullable Pair<PsiElement, PsiElement> elementRange) {
+ if (elementRange == null) {
+ return false;
+ }
+ return elementRange.getFirst().getParent() == elementRange.getSecond().getParent();
+ }
+}
diff --git a/json/src/com/intellij/json/editor/selection/JsonBasicWordSelectionFilter.java b/json/src/com/intellij/json/editor/selection/JsonBasicWordSelectionFilter.java
new file mode 100644
index 00000000..ada84f24
--- /dev/null
+++ b/json/src/com/intellij/json/editor/selection/JsonBasicWordSelectionFilter.java
@@ -0,0 +1,16 @@
+package com.intellij.json.editor.selection;
+
+import com.intellij.json.JsonParserDefinition;
+import com.intellij.openapi.util.Condition;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiUtilCore;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonBasicWordSelectionFilter implements Condition<PsiElement> {
+ @Override
+ public boolean value(PsiElement element) {
+ return !(JsonParserDefinition.STRING_LITERALS.contains(PsiUtilCore.getElementType(element)));
+ }
+}
diff --git a/json/src/com/intellij/json/editor/selection/JsonStringLiteralSelectionHandler.java b/json/src/com/intellij/json/editor/selection/JsonStringLiteralSelectionHandler.java
new file mode 100644
index 00000000..aa0fc8f3
--- /dev/null
+++ b/json/src/com/intellij/json/editor/selection/JsonStringLiteralSelectionHandler.java
@@ -0,0 +1,43 @@
+package com.intellij.json.editor.selection;
+
+import com.intellij.codeInsight.editorActions.ExtendWordSelectionHandlerBase;
+import com.intellij.codeInsight.editorActions.SelectWordUtil;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.lang.injection.InjectedLanguageManager;
+import com.intellij.lexer.StringLiteralLexer;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.ElementManipulators;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.intellij.json.JsonElementTypes.SINGLE_QUOTED_STRING;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonStringLiteralSelectionHandler extends ExtendWordSelectionHandlerBase {
+ @Override
+ public boolean canSelect(@NotNull PsiElement e) {
+ if (!(e.getParent() instanceof JsonStringLiteral)) {
+ return false;
+ }
+ return !InjectedLanguageManager.getInstance(e.getProject()).isInjectedFragment(e.getContainingFile());
+ }
+
+ @Override
+ public List<TextRange> select(@NotNull PsiElement e, @NotNull CharSequence editorText, int cursorOffset, @NotNull Editor editor) {
+ final IElementType type = e.getNode().getElementType();
+ final StringLiteralLexer lexer = new StringLiteralLexer(type == SINGLE_QUOTED_STRING ? '\'' : '"', type, false, "/", false, false);
+ final List<TextRange> result = new ArrayList<>();
+ SelectWordUtil.addWordHonoringEscapeSequences(editorText, e.getTextRange(), cursorOffset, lexer, result);
+
+ final PsiElement parent = e.getParent();
+ result.add(ElementManipulators.getValueTextRange(parent).shiftRight(parent.getTextOffset()));
+ return result;
+ }
+}
diff --git a/json/src/com/intellij/json/editor/smartEnter/JsonSmartEnterProcessor.java b/json/src/com/intellij/json/editor/smartEnter/JsonSmartEnterProcessor.java
new file mode 100644
index 00000000..ed695d74
--- /dev/null
+++ b/json/src/com/intellij/json/editor/smartEnter/JsonSmartEnterProcessor.java
@@ -0,0 +1,123 @@
+package com.intellij.json.editor.smartEnter;
+
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.lang.SmartEnterProcessorWithFixers;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiWhiteSpace;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+import static com.intellij.json.JsonElementTypes.COLON;
+import static com.intellij.json.JsonElementTypes.COMMA;
+
+/**
+ * This processor allows
+ * <ul>
+ * <li>Insert colon after key inside object property</li>
+ * <li>Insert comma after array element or object property</li>
+ * </ul>
+ *
+ * @author Mikhail Golubev
+ */
+public class JsonSmartEnterProcessor extends SmartEnterProcessorWithFixers {
+ public static final Logger LOG = Logger.getInstance(JsonSmartEnterProcessor.class);
+
+ private boolean myShouldAddNewline = false;
+
+ public JsonSmartEnterProcessor() {
+ addFixers(new JsonObjectPropertyFixer(), new JsonArrayElementFixer());
+ addEnterProcessors(new JsonEnterProcessor());
+ }
+
+ @Override
+ protected void collectAdditionalElements(@NotNull PsiElement element, @NotNull List<PsiElement> result) {
+ // include all parents as well
+ PsiElement parent = element.getParent();
+ while (parent != null && !(parent instanceof JsonFile)) {
+ result.add(parent);
+ parent = parent.getParent();
+ }
+ }
+
+ private static boolean terminatedOnCurrentLine(@NotNull Editor editor, @NotNull PsiElement element) {
+ final Document document = editor.getDocument();
+ final int caretOffset = editor.getCaretModel().getCurrentCaret().getOffset();
+ final int elementEndOffset = element.getTextRange().getEndOffset();
+ if (document.getLineNumber(elementEndOffset) != document.getLineNumber(caretOffset)) {
+ return false;
+ }
+ // Skip empty PsiError elements if comma is missing
+ PsiElement nextLeaf = PsiTreeUtil.nextLeaf(element, true);
+ return nextLeaf == null || (nextLeaf instanceof PsiWhiteSpace && nextLeaf.getText().contains("\n"));
+ }
+
+ private static boolean isFollowedByTerminal(@NotNull PsiElement element, IElementType type) {
+ final PsiElement nextLeaf = PsiTreeUtil.nextVisibleLeaf(element);
+ return nextLeaf != null && nextLeaf.getNode().getElementType() == type;
+ }
+
+ private static class JsonArrayElementFixer extends SmartEnterProcessorWithFixers.Fixer<JsonSmartEnterProcessor> {
+ @Override
+ public void apply(@NotNull Editor editor, @NotNull JsonSmartEnterProcessor processor, @NotNull PsiElement element)
+ throws IncorrectOperationException {
+ if (element instanceof JsonValue && element.getParent() instanceof JsonArray) {
+ final JsonValue arrayElement = (JsonValue)element;
+ if (terminatedOnCurrentLine(editor, arrayElement) && !isFollowedByTerminal(element, COMMA)) {
+ editor.getDocument().insertString(arrayElement.getTextRange().getEndOffset(), ",");
+ processor.myShouldAddNewline = true;
+ }
+ }
+ }
+ }
+
+ private class JsonObjectPropertyFixer extends SmartEnterProcessorWithFixers.Fixer<JsonSmartEnterProcessor> {
+ @Override
+ public void apply(@NotNull Editor editor, @NotNull JsonSmartEnterProcessor processor, @NotNull PsiElement element)
+ throws IncorrectOperationException {
+ if (element instanceof JsonProperty) {
+ final JsonValue propertyValue = ((JsonProperty)element).getValue();
+ if (propertyValue != null) {
+ if (terminatedOnCurrentLine(editor, propertyValue) && !isFollowedByTerminal(propertyValue, COMMA)) {
+ editor.getDocument().insertString(propertyValue.getTextRange().getEndOffset(), ",");
+ processor.myShouldAddNewline = true;
+ }
+ }
+ else {
+ final JsonValue propertyKey = ((JsonProperty)element).getNameElement();
+ final int keyEndOffset = propertyKey.getTextRange().getEndOffset();
+ //processor.myFirstErrorOffset = keyEndOffset;
+ if (terminatedOnCurrentLine(editor, propertyKey) && !isFollowedByTerminal(propertyKey, COLON)) {
+ processor.myFirstErrorOffset = keyEndOffset + 2;
+ editor.getDocument().insertString(keyEndOffset, ": ");
+ }
+ }
+ }
+ }
+ }
+
+ private class JsonEnterProcessor extends SmartEnterProcessorWithFixers.FixEnterProcessor {
+ @Override
+ public boolean doEnter(PsiElement atCaret, PsiFile file, @NotNull Editor editor, boolean modified) {
+ if (myShouldAddNewline) {
+ try {
+ plainEnter(editor);
+ }
+ finally {
+ myShouldAddNewline = false;
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/findUsages/JsonFindUsagesProvider.java b/json/src/com/intellij/json/findUsages/JsonFindUsagesProvider.java
new file mode 100644
index 00000000..fb7dfd3c
--- /dev/null
+++ b/json/src/com/intellij/json/findUsages/JsonFindUsagesProvider.java
@@ -0,0 +1,54 @@
+package com.intellij.json.findUsages;
+
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.lang.HelpID;
+import com.intellij.lang.cacheBuilder.WordsScanner;
+import com.intellij.lang.findUsages.FindUsagesProvider;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiNamedElement;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonFindUsagesProvider implements FindUsagesProvider {
+ @Nullable
+ @Override
+ public WordsScanner getWordsScanner() {
+ return new JsonWordScanner();
+ }
+
+ @Override
+ public boolean canFindUsagesFor(@NotNull PsiElement psiElement) {
+ return psiElement instanceof PsiNamedElement;
+ }
+
+ @Nullable
+ @Override
+ public String getHelpId(@NotNull PsiElement psiElement) {
+ return HelpID.FIND_OTHER_USAGES;
+ }
+
+ @NotNull
+ @Override
+ public String getType(@NotNull PsiElement element) {
+ if (element instanceof JsonProperty) {
+ return "property";
+ }
+ return "";
+ }
+
+ @NotNull
+ @Override
+ public String getDescriptiveName(@NotNull PsiElement element) {
+ final String name = element instanceof PsiNamedElement ? ((PsiNamedElement)element).getName() : null;
+ return name != null ? name : "<unnamed>";
+ }
+
+ @NotNull
+ @Override
+ public String getNodeText(@NotNull PsiElement element, boolean useFullName) {
+ return getDescriptiveName(element);
+ }
+}
diff --git a/json/src/com/intellij/json/findUsages/JsonWordScanner.java b/json/src/com/intellij/json/findUsages/JsonWordScanner.java
new file mode 100644
index 00000000..df570922
--- /dev/null
+++ b/json/src/com/intellij/json/findUsages/JsonWordScanner.java
@@ -0,0 +1,19 @@
+package com.intellij.json.findUsages;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.JsonLexer;
+import com.intellij.lang.cacheBuilder.DefaultWordsScanner;
+import com.intellij.psi.tree.TokenSet;
+
+import static com.intellij.json.JsonParserDefinition.JSON_COMMENTARIES;
+import static com.intellij.json.JsonParserDefinition.JSON_LITERALS;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonWordScanner extends DefaultWordsScanner {
+ public JsonWordScanner() {
+ super(new JsonLexer(), TokenSet.create(JsonElementTypes.IDENTIFIER), JSON_COMMENTARIES, JSON_LITERALS);
+ setMayHaveFileRefsInLiterals(true);
+ }
+}
diff --git a/json/src/com/intellij/json/formatter/JsonBlock.java b/json/src/com/intellij/json/formatter/JsonBlock.java
new file mode 100644
index 00000000..992e0001
--- /dev/null
+++ b/json/src/com/intellij/json/formatter/JsonBlock.java
@@ -0,0 +1,221 @@
+package com.intellij.json.formatter;
+
+import com.intellij.formatting.*;
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonPsiUtil;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.TokenType;
+import com.intellij.psi.codeStyle.CodeStyleSettings;
+import com.intellij.psi.tree.TokenSet;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+import static com.intellij.json.JsonElementTypes.*;
+import static com.intellij.json.JsonParserDefinition.JSON_CONTAINERS;
+import static com.intellij.json.formatter.JsonCodeStyleSettings.ALIGN_PROPERTY_ON_COLON;
+import static com.intellij.json.formatter.JsonCodeStyleSettings.ALIGN_PROPERTY_ON_VALUE;
+import static com.intellij.json.psi.JsonPsiUtil.hasElementType;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonBlock implements ASTBlock {
+ private static final TokenSet JSON_OPEN_BRACES = TokenSet.create(L_BRACKET, L_CURLY);
+ private static final TokenSet JSON_CLOSE_BRACES = TokenSet.create(R_BRACKET, R_CURLY);
+ private static final TokenSet JSON_ALL_BRACES = TokenSet.orSet(JSON_OPEN_BRACES, JSON_CLOSE_BRACES);
+
+ private final JsonBlock myParent;
+
+ private final ASTNode myNode;
+ private final PsiElement myPsiElement;
+ private final Alignment myAlignment;
+ private final Indent myIndent;
+ private final Wrap myWrap;
+ private final JsonCodeStyleSettings myCustomSettings;
+ private final SpacingBuilder mySpacingBuilder;
+ // lazy initialized on first call to #getSubBlocks()
+ private List<Block> mySubBlocks = null;
+
+ private final Alignment myPropertyValueAlignment;
+ private final Wrap myChildWrap;
+
+ /**
+ * @deprecated Please use overload with settings JsonCodeStyleSettings and spacingBuilder.
+ * Getting settings should be done only for the root block.
+ */
+ @Deprecated
+ @SuppressWarnings("unused") //used externally
+ public JsonBlock(@Nullable JsonBlock parent,
+ @NotNull ASTNode node,
+ @NotNull CodeStyleSettings settings,
+ @Nullable Alignment alignment,
+ @NotNull Indent indent,
+ @Nullable Wrap wrap) {
+ this(parent, node, settings.getCustomSettings(JsonCodeStyleSettings.class), alignment, indent, wrap,
+ JsonFormattingBuilderModel.createSpacingBuilder(settings));
+ }
+
+ public JsonBlock(@Nullable JsonBlock parent,
+ @NotNull ASTNode node,
+ @NotNull JsonCodeStyleSettings customSettings,
+ @Nullable Alignment alignment,
+ @NotNull Indent indent,
+ @Nullable Wrap wrap,
+ @NotNull SpacingBuilder spacingBuilder) {
+ myParent = parent;
+ myNode = node;
+ myPsiElement = node.getPsi();
+ myAlignment = alignment;
+ myIndent = indent;
+ myWrap = wrap;
+ mySpacingBuilder = spacingBuilder;
+ myCustomSettings = customSettings;
+
+ if (myPsiElement instanceof JsonObject) {
+ myChildWrap = Wrap.createWrap(myCustomSettings.OBJECT_WRAPPING, true);
+ }
+ else if (myPsiElement instanceof JsonArray) {
+ myChildWrap = Wrap.createWrap(myCustomSettings.ARRAY_WRAPPING, true);
+ }
+ else {
+ myChildWrap = null;
+ }
+
+ myPropertyValueAlignment = myPsiElement instanceof JsonObject ? Alignment.createAlignment(true) : null;
+ }
+
+ @Override
+ public ASTNode getNode() {
+ return myNode;
+ }
+
+ @NotNull
+ @Override
+ public TextRange getTextRange() {
+ return myNode.getTextRange();
+ }
+
+ @NotNull
+ @Override
+ public List<Block> getSubBlocks() {
+ if (mySubBlocks == null) {
+ int propertyAlignment = myCustomSettings.PROPERTY_ALIGNMENT;
+ ASTNode[] children = myNode.getChildren(null);
+ mySubBlocks = ContainerUtil.newArrayListWithCapacity(children.length);
+ for (ASTNode child: children) {
+ if (isWhitespaceOrEmpty(child)) continue;
+ mySubBlocks.add(makeSubBlock(child, propertyAlignment));
+ }
+ }
+ return mySubBlocks;
+ }
+
+ private Block makeSubBlock(@NotNull ASTNode childNode, int propertyAlignment) {
+ Indent indent = Indent.getNoneIndent();
+ Alignment alignment = null;
+ Wrap wrap = null;
+
+ if (hasElementType(myNode, JSON_CONTAINERS)) {
+ if (hasElementType(childNode, COMMA)) {
+ wrap = Wrap.createWrap(WrapType.NONE, true);
+ }
+ else if (!hasElementType(childNode, JSON_ALL_BRACES)) {
+ assert myChildWrap != null;
+ wrap = myChildWrap;
+ indent = Indent.getNormalIndent();
+ }
+ else if (hasElementType(childNode, JSON_OPEN_BRACES)) {
+ if (JsonPsiUtil.isPropertyValue(myPsiElement) && propertyAlignment == ALIGN_PROPERTY_ON_VALUE) {
+ // WEB-13587 Align compound values on opening brace/bracket, not the whole block
+ assert myParent != null && myParent.myParent != null && myParent.myParent.myPropertyValueAlignment != null;
+ alignment = myParent.myParent.myPropertyValueAlignment;
+ }
+ }
+ }
+ // Handle properties alignment
+ else if (hasElementType(myNode, PROPERTY) ) {
+ assert myParent != null && myParent.myPropertyValueAlignment != null;
+ if (hasElementType(childNode, COLON) && propertyAlignment == ALIGN_PROPERTY_ON_COLON) {
+ alignment = myParent.myPropertyValueAlignment;
+ }
+ else if (JsonPsiUtil.isPropertyValue(childNode.getPsi()) && propertyAlignment == ALIGN_PROPERTY_ON_VALUE) {
+ if (!hasElementType(childNode, JSON_CONTAINERS)) {
+ alignment = myParent.myPropertyValueAlignment;
+ }
+ }
+ }
+ return new JsonBlock(this, childNode, myCustomSettings, alignment, indent, wrap, mySpacingBuilder);
+ }
+
+ @Nullable
+ @Override
+ public Wrap getWrap() {
+ return myWrap;
+ }
+
+ @Nullable
+ @Override
+ public Indent getIndent() {
+ return myIndent;
+ }
+
+ @Nullable
+ @Override
+ public Alignment getAlignment() {
+ return myAlignment;
+ }
+
+ @Nullable
+ @Override
+ public Spacing getSpacing(@Nullable Block child1, @NotNull Block child2) {
+ return mySpacingBuilder.getSpacing(this, child1, child2);
+ }
+
+ @NotNull
+ @Override
+ public ChildAttributes getChildAttributes(int newChildIndex) {
+ if (hasElementType(myNode, JSON_CONTAINERS)) {
+ // WEB-13675: For some reason including alignment in child attributes causes
+ // indents to consist solely of spaces when both USE_TABS and SMART_TAB
+ // options are enabled.
+ return new ChildAttributes(Indent.getNormalIndent(), null);
+ }
+ else if (myNode.getPsi() instanceof PsiFile) {
+ return new ChildAttributes(Indent.getNoneIndent(), null);
+ }
+ // Will use continuation indent for cases like { "foo"<caret> }
+ return new ChildAttributes(null, null);
+ }
+
+ @Override
+ public boolean isIncomplete() {
+ final ASTNode lastChildNode = myNode.getLastChildNode();
+ if (hasElementType(myNode, OBJECT)) {
+ return lastChildNode != null && lastChildNode.getElementType() != R_CURLY;
+ }
+ else if (hasElementType(myNode, ARRAY)) {
+ return lastChildNode != null && lastChildNode.getElementType() != R_BRACKET;
+ }
+ else if (hasElementType(myNode, PROPERTY)) {
+ return ((JsonProperty)myPsiElement).getValue() == null;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return myNode.getFirstChildNode() == null;
+ }
+
+ private static boolean isWhitespaceOrEmpty(ASTNode node) {
+ return node.getElementType() == TokenType.WHITE_SPACE || node.getTextLength() == 0;
+ }
+}
diff --git a/json/src/com/intellij/json/formatter/JsonCodeStyleSettings.java b/json/src/com/intellij/json/formatter/JsonCodeStyleSettings.java
new file mode 100644
index 00000000..fa19c5e6
--- /dev/null
+++ b/json/src/com/intellij/json/formatter/JsonCodeStyleSettings.java
@@ -0,0 +1,78 @@
+package com.intellij.json.formatter;
+
+import com.intellij.json.JsonBundle;
+import com.intellij.json.JsonLanguage;
+import com.intellij.psi.codeStyle.CodeStyleSettings;
+import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
+import com.intellij.psi.codeStyle.CustomCodeStyleSettings;
+import org.intellij.lang.annotations.MagicConstant;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonCodeStyleSettings extends CustomCodeStyleSettings {
+
+ public static int DO_NOT_ALIGN_PROPERTY = PropertyAlignment.DO_NOT_ALIGN.getId();
+ public static int ALIGN_PROPERTY_ON_VALUE = PropertyAlignment.ALIGN_ON_VALUE.getId();
+ public static int ALIGN_PROPERTY_ON_COLON = PropertyAlignment.ALIGN_ON_COLON.getId();
+
+ public boolean SPACE_AFTER_COLON = true;
+ public boolean SPACE_BEFORE_COLON = false;
+ public boolean KEEP_TRAILING_COMMA = false;
+
+ // TODO: check whether it's possible to migrate CustomCodeStyleSettings to newer com.intellij.util.xmlb.XmlSerializer
+ /**
+ * Contains value of {@link com.intellij.json.formatter.JsonCodeStyleSettings.PropertyAlignment#getId()}
+ *
+ * @see #DO_NOT_ALIGN_PROPERTY
+ * @see #ALIGN_PROPERTY_ON_VALUE
+ * @see #ALIGN_PROPERTY_ON_COLON
+ */
+ public int PROPERTY_ALIGNMENT = PropertyAlignment.DO_NOT_ALIGN.getId();
+
+ @MagicConstant(flags = {
+ CommonCodeStyleSettings.DO_NOT_WRAP,
+ CommonCodeStyleSettings.WRAP_ALWAYS,
+ CommonCodeStyleSettings.WRAP_AS_NEEDED,
+ CommonCodeStyleSettings.WRAP_ON_EVERY_ITEM
+ })
+ public int OBJECT_WRAPPING = CommonCodeStyleSettings.WRAP_ALWAYS;
+
+ // This was default policy for array elements wrapping in JavaScript's JSON.
+ // CHOP_DOWN_IF_LONG seems more appropriate however for short arrays.
+ @MagicConstant(flags = {
+ CommonCodeStyleSettings.DO_NOT_WRAP,
+ CommonCodeStyleSettings.WRAP_ALWAYS,
+ CommonCodeStyleSettings.WRAP_AS_NEEDED,
+ CommonCodeStyleSettings.WRAP_ON_EVERY_ITEM
+ })
+ public int ARRAY_WRAPPING = CommonCodeStyleSettings.WRAP_ALWAYS;
+
+ public JsonCodeStyleSettings(CodeStyleSettings container) {
+ super(JsonLanguage.INSTANCE.getID(), container);
+ }
+
+ public enum PropertyAlignment {
+ DO_NOT_ALIGN(JsonBundle.message("formatter.align.properties.none"), 0),
+ ALIGN_ON_VALUE(JsonBundle.message("formatter.align.properties.on.value"), 1),
+ ALIGN_ON_COLON(JsonBundle.message("formatter.align.properties.on.colon"), 2);
+
+ private final String myDescription;
+ private final int myId;
+
+ PropertyAlignment(@NotNull String description, int id) {
+ myDescription = description;
+ myId = id;
+ }
+
+ @NotNull
+ public String getDescription() {
+ return myDescription;
+ }
+
+ public int getId() {
+ return myId;
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/formatter/JsonCodeStyleSettingsProvider.java b/json/src/com/intellij/json/formatter/JsonCodeStyleSettingsProvider.java
new file mode 100644
index 00000000..6a54a233
--- /dev/null
+++ b/json/src/com/intellij/json/formatter/JsonCodeStyleSettingsProvider.java
@@ -0,0 +1,57 @@
+package com.intellij.json.formatter;
+
+import com.intellij.application.options.CodeStyleAbstractConfigurable;
+import com.intellij.application.options.CodeStyleAbstractPanel;
+import com.intellij.application.options.TabbedLanguageCodeStylePanel;
+import com.intellij.json.JsonLanguage;
+import com.intellij.lang.Language;
+import com.intellij.openapi.options.Configurable;
+import com.intellij.psi.codeStyle.CodeStyleSettings;
+import com.intellij.psi.codeStyle.CodeStyleSettingsProvider;
+import com.intellij.psi.codeStyle.CustomCodeStyleSettings;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonCodeStyleSettingsProvider extends CodeStyleSettingsProvider {
+ @NotNull
+ @Override
+ public Configurable createSettingsPage(CodeStyleSettings settings, CodeStyleSettings originalSettings) {
+ return new CodeStyleAbstractConfigurable(settings, originalSettings, "JSON") {
+ @Override
+ protected CodeStyleAbstractPanel createPanel(CodeStyleSettings settings) {
+ final Language language = JsonLanguage.INSTANCE;
+ final CodeStyleSettings currentSettings = getCurrentSettings();
+ return new TabbedLanguageCodeStylePanel(language, currentSettings, settings) {
+ @Override
+ protected void initTabs(CodeStyleSettings settings) {
+ addIndentOptionsTab(settings);
+ addSpacesTab(settings);
+ addBlankLinesTab(settings);
+ addWrappingAndBracesTab(settings);
+ }
+ };
+ }
+
+ @Nullable
+ @Override
+ public String getHelpTopic() {
+ return "reference.settingsdialog.codestyle.json";
+ }
+ };
+ }
+
+ @Nullable
+ @Override
+ public String getConfigurableDisplayName() {
+ return JsonLanguage.INSTANCE.getDisplayName();
+ }
+
+ @Nullable
+ @Override
+ public CustomCodeStyleSettings createCustomSettings(CodeStyleSettings settings) {
+ return new JsonCodeStyleSettings(settings);
+ }
+}
diff --git a/json/src/com/intellij/json/formatter/JsonFormattingBuilderModel.java b/json/src/com/intellij/json/formatter/JsonFormattingBuilderModel.java
new file mode 100644
index 00000000..ed8d7503
--- /dev/null
+++ b/json/src/com/intellij/json/formatter/JsonFormattingBuilderModel.java
@@ -0,0 +1,42 @@
+package com.intellij.json.formatter;
+
+import com.intellij.formatting.*;
+import com.intellij.json.JsonLanguage;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.codeStyle.CodeStyleSettings;
+import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
+import org.jetbrains.annotations.NotNull;
+
+import static com.intellij.json.JsonElementTypes.*;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonFormattingBuilderModel implements FormattingModelBuilder {
+ @NotNull
+ @Override
+ public FormattingModel createModel(PsiElement element, CodeStyleSettings settings) {
+ JsonCodeStyleSettings customSettings = settings.getCustomSettings(JsonCodeStyleSettings.class);
+ SpacingBuilder spacingBuilder = createSpacingBuilder(settings);
+ final JsonBlock block = new JsonBlock(null, element.getNode(), customSettings, null, Indent.getSmartIndent(Indent.Type.CONTINUATION), null, spacingBuilder);
+ return FormattingModelProvider.createFormattingModelForPsiFile(element.getContainingFile(), block, settings);
+ }
+
+ @NotNull
+ static SpacingBuilder createSpacingBuilder(CodeStyleSettings settings) {
+ final JsonCodeStyleSettings jsonSettings = settings.getCustomSettings(JsonCodeStyleSettings.class);
+ final CommonCodeStyleSettings commonSettings = settings.getCommonSettings(JsonLanguage.INSTANCE);
+
+ final int spacesBeforeComma = commonSettings.SPACE_BEFORE_COMMA ? 1 : 0;
+ final int spacesBeforeColon = jsonSettings.SPACE_BEFORE_COLON ? 1 : 0;
+ final int spacesAfterColon = jsonSettings.SPACE_AFTER_COLON ? 1 : 0;
+
+ return new SpacingBuilder(settings, JsonLanguage.INSTANCE)
+ .before(COLON).spacing(spacesBeforeColon, spacesBeforeColon, 0, false, 0)
+ .after(COLON).spacing(spacesAfterColon, spacesAfterColon, 0, false, 0)
+ .withinPair(L_BRACKET, R_BRACKET).spaceIf(commonSettings.SPACE_WITHIN_BRACKETS, true)
+ .withinPair(L_CURLY, R_CURLY).spaceIf(commonSettings.SPACE_WITHIN_BRACES, true)
+ .before(COMMA).spacing(spacesBeforeComma, spacesBeforeComma, 0, false, 0)
+ .after(COMMA).spaceIf(commonSettings.SPACE_AFTER_COMMA);
+ }
+}
diff --git a/json/src/com/intellij/json/formatter/JsonLanguageCodeStyleSettingsProvider.java b/json/src/com/intellij/json/formatter/JsonLanguageCodeStyleSettingsProvider.java
new file mode 100644
index 00000000..eb727926
--- /dev/null
+++ b/json/src/com/intellij/json/formatter/JsonLanguageCodeStyleSettingsProvider.java
@@ -0,0 +1,116 @@
+package com.intellij.json.formatter;
+
+import com.intellij.application.options.IndentOptionsEditor;
+import com.intellij.application.options.SmartIndentOptionsEditor;
+import com.intellij.json.JsonBundle;
+import com.intellij.json.JsonLanguage;
+import com.intellij.lang.Language;
+import com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable;
+import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
+import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider;
+import com.intellij.util.ArrayUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+import static com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable.SPACES_OTHER;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettingsProvider {
+ private static final String[] ALIGN_OPTIONS = Arrays.stream(JsonCodeStyleSettings.PropertyAlignment.values())
+ .map(alignment -> alignment.getDescription())
+ .toArray(value -> new String[value]);
+
+ private static final int[] ALIGN_VALUES =
+ ArrayUtil.toIntArray(Arrays.stream(JsonCodeStyleSettings.PropertyAlignment.values())
+ .map(alignment -> alignment.getId())
+ .collect(Collectors.toList()));
+
+ private static final String SAMPLE = "{\n" +
+ " \"json literals are\": {\n" +
+ " \"strings\": [\"foo\", \"bar\", \"\\u0062\\u0061\\u0072\"],\n" +
+ " \"numbers\": [42, 6.62606975e-34],\n" +
+ " \"boolean values\": [true, false,],\n" +
+ " \"objects\": {\"null\": null,\"another\": null,}\n" +
+ " }\n" +
+ "}";
+
+
+ @Override
+ public void customizeSettings(@NotNull CodeStyleSettingsCustomizable consumer, @NotNull SettingsType settingsType) {
+ if (settingsType == SettingsType.SPACING_SETTINGS) {
+ consumer.showStandardOptions("SPACE_WITHIN_BRACKETS",
+ "SPACE_WITHIN_BRACES",
+ "SPACE_AFTER_COMMA",
+ "SPACE_BEFORE_COMMA");
+ consumer.renameStandardOption("SPACE_WITHIN_BRACES", "Braces");
+ consumer.showCustomOption(JsonCodeStyleSettings.class, "SPACE_BEFORE_COLON", "Before ':'", SPACES_OTHER);
+ consumer.showCustomOption(JsonCodeStyleSettings.class, "SPACE_AFTER_COLON", "After ':'", SPACES_OTHER);
+ }
+ else if (settingsType == SettingsType.BLANK_LINES_SETTINGS) {
+ consumer.showStandardOptions("KEEP_BLANK_LINES_IN_CODE");
+ }
+ else if (settingsType == SettingsType.WRAPPING_AND_BRACES_SETTINGS) {
+ consumer.showStandardOptions("RIGHT_MARGIN",
+ "WRAP_ON_TYPING",
+ "KEEP_LINE_BREAKS",
+ "WRAP_LONG_LINES");
+
+ consumer.showCustomOption(JsonCodeStyleSettings.class,
+ "KEEP_TRAILING_COMMA",
+ "Trailing comma",
+ CodeStyleSettingsCustomizable.WRAPPING_KEEP);
+
+ consumer.showCustomOption(JsonCodeStyleSettings.class,
+ "ARRAY_WRAPPING",
+ "Arrays",
+ null,
+ CodeStyleSettingsCustomizable.WRAP_OPTIONS,
+ CodeStyleSettingsCustomizable.WRAP_VALUES);
+
+ consumer.showCustomOption(JsonCodeStyleSettings.class,
+ "OBJECT_WRAPPING",
+ "Objects",
+ null,
+ CodeStyleSettingsCustomizable.WRAP_OPTIONS,
+ CodeStyleSettingsCustomizable.WRAP_VALUES);
+
+ consumer.showCustomOption(JsonCodeStyleSettings.class,
+ "PROPERTY_ALIGNMENT",
+ JsonBundle.message("formatter.align.properties.caption"),
+ "Objects",
+ ALIGN_OPTIONS,
+ ALIGN_VALUES);
+
+ }
+ }
+
+ @NotNull
+ @Override
+ public Language getLanguage() {
+ return JsonLanguage.INSTANCE;
+ }
+
+ @Nullable
+ @Override
+ public IndentOptionsEditor getIndentOptionsEditor() {
+ return new SmartIndentOptionsEditor();
+ }
+
+ @Override
+ public String getCodeSample(@NotNull SettingsType settingsType) {
+ return SAMPLE;
+ }
+
+ @Override
+ protected void customizeDefaults(@NotNull CommonCodeStyleSettings commonSettings,
+ @NotNull CommonCodeStyleSettings.IndentOptions indentOptions) {
+ indentOptions.INDENT_SIZE = 2;
+ // strip all blank lines by default
+ commonSettings.KEEP_BLANK_LINES_IN_CODE = 0;
+ }
+}
diff --git a/json/src/com/intellij/json/formatter/JsonLineWrapPositionStrategy.java b/json/src/com/intellij/json/formatter/JsonLineWrapPositionStrategy.java
new file mode 100644
index 00000000..3116dc52
--- /dev/null
+++ b/json/src/com/intellij/json/formatter/JsonLineWrapPositionStrategy.java
@@ -0,0 +1,70 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.formatter;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.openapi.editor.DefaultLineWrapPositionStrategy;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiComment;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.psi.util.PsiUtilCore;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class JsonLineWrapPositionStrategy extends DefaultLineWrapPositionStrategy {
+ @Override
+ public int calculateWrapPosition(@NotNull Document document,
+ @Nullable Project project,
+ int startOffset,
+ int endOffset,
+ int maxPreferredOffset,
+ boolean allowToBeyondMaxPreferredOffset,
+ boolean isSoftWrap) {
+ if (isSoftWrap) {
+ return super.calculateWrapPosition(document, project, startOffset, endOffset, maxPreferredOffset, allowToBeyondMaxPreferredOffset,
+ true);
+ }
+ if (project == null) return -1;
+ final int wrapPosition = getMinWrapPosition(document, project, maxPreferredOffset);
+ if (wrapPosition == SKIP_WRAPPING) return -1;
+ int minWrapPosition = Math.max(startOffset, wrapPosition);
+ return super
+ .calculateWrapPosition(document, project, minWrapPosition, endOffset, maxPreferredOffset, allowToBeyondMaxPreferredOffset, isSoftWrap);
+ }
+
+ private static final int SKIP_WRAPPING = -2;
+ private static int getMinWrapPosition(@NotNull Document document, @NotNull Project project, int offset) {
+ PsiDocumentManager manager = PsiDocumentManager.getInstance(project);
+ if (manager.isUncommited(document)) manager.commitDocument(document);
+ PsiFile psiFile = manager.getPsiFile(document);
+ if (psiFile != null) {
+ PsiElement currElement = psiFile.findElementAt(offset);
+ final IElementType elementType = PsiUtilCore.getElementType(currElement);
+ if (elementType == JsonElementTypes.DOUBLE_QUOTED_STRING
+ || elementType == JsonElementTypes.SINGLE_QUOTED_STRING
+ || elementType == JsonElementTypes.LITERAL
+ || elementType == JsonElementTypes.BOOLEAN_LITERAL
+ || elementType == JsonElementTypes.TRUE
+ || elementType == JsonElementTypes.FALSE
+ || elementType == JsonElementTypes.IDENTIFIER
+ || elementType == JsonElementTypes.NULL_LITERAL
+ || elementType == JsonElementTypes.NUMBER_LITERAL) {
+ return currElement.getTextRange().getEndOffset();
+ }
+ if (elementType == JsonElementTypes.COLON) {
+ return SKIP_WRAPPING;
+ }
+ if (currElement != null) {
+ if (currElement instanceof PsiComment ||
+ PsiUtilCore.getElementType(PsiTreeUtil.skipWhitespacesForward(currElement)) == JsonElementTypes.COMMA) {
+ return SKIP_WRAPPING;
+ }
+ }
+ }
+ return -1;
+ }
+}
diff --git a/json/src/com/intellij/json/formatter/JsonTrailingCommaRemover.java b/json/src/com/intellij/json/formatter/JsonTrailingCommaRemover.java
new file mode 100644
index 00000000..b3423fd9
--- /dev/null
+++ b/json/src/com/intellij/json/formatter/JsonTrailingCommaRemover.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2000-2017 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.intellij.json.formatter;
+
+import com.intellij.application.options.CodeStyle;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.JsonLanguage;
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.impl.JsonRecursiveElementVisitor;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.*;
+import com.intellij.psi.impl.source.codeStyle.PreFormatProcessor;
+import com.intellij.util.DocumentUtil;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class JsonTrailingCommaRemover implements PreFormatProcessor {
+
+ @NotNull
+ @Override
+ public TextRange process(@NotNull ASTNode element, @NotNull TextRange range) {
+ PsiElement rootPsi = element.getPsi();
+ if (rootPsi.getLanguage() != JsonLanguage.INSTANCE) {
+ return range;
+ }
+ JsonCodeStyleSettings settings = CodeStyle.getCustomSettings(rootPsi.getContainingFile(), JsonCodeStyleSettings.class);
+ if (settings.KEEP_TRAILING_COMMA) {
+ return range;
+ }
+ PsiDocumentManager psiDocumentManager = PsiDocumentManager.getInstance(rootPsi.getProject());
+ Document document = psiDocumentManager.getDocument(rootPsi.getContainingFile());
+ if (document == null) {
+ return range;
+ }
+ DocumentUtil.executeInBulk(document, true, () -> {
+ psiDocumentManager.doPostponedOperationsAndUnblockDocument(document);
+ PsiElementVisitor visitor = new Visitor(document);
+ rootPsi.accept(visitor);
+ psiDocumentManager.commitDocument(document);
+ });
+ return range;
+ }
+
+ private static class Visitor extends JsonRecursiveElementVisitor {
+ private final Document myDocument;
+ private int myOffsetDelta;
+
+ Visitor(Document document) {
+ myDocument = document;
+ }
+
+ @Override
+ public void visitArray(@NotNull JsonArray o) {
+ super.visitArray(o);
+ PsiElement lastChild = o.getLastChild();
+ if (lastChild == null || lastChild.getNode().getElementType() != JsonElementTypes.R_BRACKET) {
+ return;
+ }
+ deleteTrailingCommas(ObjectUtils.coalesce(ContainerUtil.getLastItem(o.getValueList()), o.getFirstChild()));
+ }
+
+ @Override
+ public void visitObject(@NotNull JsonObject o) {
+ super.visitObject(o);
+ PsiElement lastChild = o.getLastChild();
+ if (lastChild == null || lastChild.getNode().getElementType() != JsonElementTypes.R_CURLY) {
+ return;
+ }
+ deleteTrailingCommas(ObjectUtils.coalesce(ContainerUtil.getLastItem(o.getPropertyList()), o.getFirstChild()));
+ }
+
+ private void deleteTrailingCommas(@Nullable PsiElement lastElementOrOpeningBrace) {
+ PsiElement element = lastElementOrOpeningBrace != null ? lastElementOrOpeningBrace.getNextSibling() : null;
+
+ while (element != null) {
+ if (element.getNode().getElementType() == JsonElementTypes.COMMA ||
+ element instanceof PsiErrorElement && ",".equals(element.getText())) {
+ deleteNode(element.getNode());
+ }
+ else if (!(element instanceof PsiComment || element instanceof PsiWhiteSpace)) {
+ break;
+ }
+ element = element.getNextSibling();
+ }
+ }
+
+ private void deleteNode(@NotNull ASTNode node) {
+ int length = node.getTextLength();
+ myDocument.deleteString(node.getStartOffset() + myOffsetDelta, node.getStartOffset() + length + myOffsetDelta);
+ myOffsetDelta -= length;
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/highlighting/JsonColorsPage.java b/json/src/com/intellij/json/highlighting/JsonColorsPage.java
new file mode 100644
index 00000000..69075e6c
--- /dev/null
+++ b/json/src/com/intellij/json/highlighting/JsonColorsPage.java
@@ -0,0 +1,105 @@
+package com.intellij.json.highlighting;
+
+import com.google.common.collect.ImmutableMap;
+import com.intellij.icons.AllIcons;
+import com.intellij.json.JsonLanguage;
+import com.intellij.openapi.editor.colors.TextAttributesKey;
+import com.intellij.openapi.fileTypes.SyntaxHighlighter;
+import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory;
+import com.intellij.openapi.options.colors.AttributesDescriptor;
+import com.intellij.openapi.options.colors.ColorDescriptor;
+import com.intellij.openapi.options.colors.ColorSettingsPage;
+import com.intellij.psi.codeStyle.DisplayPriority;
+import com.intellij.psi.codeStyle.DisplayPrioritySortable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.Map;
+
+import static com.intellij.json.highlighting.JsonSyntaxHighlighterFactory.*;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonColorsPage implements ColorSettingsPage, DisplayPrioritySortable {
+ private static final Map<String, TextAttributesKey> ourAdditionalHighlighting = ImmutableMap.of("propertyKey", JSON_PROPERTY_KEY);
+
+ private static final AttributesDescriptor[] ourAttributeDescriptors = new AttributesDescriptor[]{
+ new AttributesDescriptor("Property key", JSON_PROPERTY_KEY),
+
+ new AttributesDescriptor("Braces", JSON_BRACES),
+ new AttributesDescriptor("Brackets", JSON_BRACKETS),
+ new AttributesDescriptor("Comma", JSON_COMMA),
+ new AttributesDescriptor("Colon", JSON_COLON),
+ new AttributesDescriptor("Number", JSON_NUMBER),
+ new AttributesDescriptor("String", JSON_STRING),
+ new AttributesDescriptor("Keyword", JSON_KEYWORD),
+ new AttributesDescriptor("Line comment", JSON_LINE_COMMENT),
+ new AttributesDescriptor("Block comment", JSON_BLOCK_COMMENT),
+ //new AttributesDescriptor("", JSON_IDENTIFIER),
+ new AttributesDescriptor("Valid escape sequence", JSON_VALID_ESCAPE),
+ new AttributesDescriptor("Invalid escape sequence", JSON_INVALID_ESCAPE),
+ };
+
+ @Nullable
+ @Override
+ public Icon getIcon() {
+ return AllIcons.FileTypes.Json;
+ }
+
+ @NotNull
+ @Override
+ public SyntaxHighlighter getHighlighter() {
+ return SyntaxHighlighterFactory.getSyntaxHighlighter(JsonLanguage.INSTANCE, null, null);
+ }
+
+ @NotNull
+ @Override
+ public String getDemoText() {
+ return "{\n" +
+ " // Line comments are not included in standard but nonetheless allowed.\n" +
+ " /* As well as block comments. */\n" +
+ " <propertyKey>\"the only keywords are\"</propertyKey>: [true, false, null],\n" +
+ " <propertyKey>\"strings with\"</propertyKey>: {\n" +
+ " <propertyKey>\"no escapes\"</propertyKey>: \"pseudopolinomiality\"\n" +
+ " <propertyKey>\"valid escapes\"</propertyKey>: \"C-style\\r\\n and unicode\\u0021\",\n" +
+ " <propertyKey>\"illegal escapes\"</propertyKey>: \"\\0377\\x\\\"\n" +
+ " },\n" +
+ " <propertyKey>\"some numbers\"</propertyKey>: [\n" +
+ " 42,\n" +
+ " -0.0e-0,\n" +
+ " 6.626e-34\n" +
+ " ] \n" +
+ "}";
+ }
+
+ @Nullable
+ @Override
+ public Map<String, TextAttributesKey> getAdditionalHighlightingTagToDescriptorMap() {
+ return ourAdditionalHighlighting;
+ }
+
+ @NotNull
+ @Override
+ public AttributesDescriptor[] getAttributeDescriptors() {
+ return ourAttributeDescriptors;
+ }
+
+ @NotNull
+ @Override
+ public ColorDescriptor[] getColorDescriptors() {
+ return ColorDescriptor.EMPTY_ARRAY;
+ }
+
+ @NotNull
+ @Override
+ public String getDisplayName() {
+ return "JSON";
+ }
+
+ @Override
+ public DisplayPriority getPriority() {
+ return DisplayPriority.LANGUAGE_SETTINGS;
+ }
+}
diff --git a/json/src/com/intellij/json/highlighting/JsonSyntaxHighlighterFactory.java b/json/src/com/intellij/json/highlighting/JsonSyntaxHighlighterFactory.java
new file mode 100644
index 00000000..fd80e8cb
--- /dev/null
+++ b/json/src/com/intellij/json/highlighting/JsonSyntaxHighlighterFactory.java
@@ -0,0 +1,164 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.highlighting;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.JsonFileType;
+import com.intellij.json.JsonLanguage;
+import com.intellij.json.JsonLexer;
+import com.intellij.lang.Language;
+import com.intellij.lexer.LayeredLexer;
+import com.intellij.lexer.Lexer;
+import com.intellij.lexer.StringLiteralLexer;
+import com.intellij.openapi.editor.HighlighterColors;
+import com.intellij.openapi.editor.colors.TextAttributesKey;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.fileTypes.SyntaxHighlighter;
+import com.intellij.openapi.fileTypes.SyntaxHighlighterBase;
+import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.StringEscapesTokenTypes;
+import com.intellij.psi.TokenType;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.intellij.openapi.editor.DefaultLanguageHighlighterColors.*;
+
+public class JsonSyntaxHighlighterFactory extends SyntaxHighlighterFactory {
+
+ private static final String PERMISSIVE_ESCAPES;
+ static {
+ final StringBuilder escapesBuilder = new StringBuilder("/");
+ for (char c = '\1'; c < '\255'; c++) {
+ if (c != 'x' && c != 'u' && !Character.isDigit(c) && c != '\n' && c != '\r') {
+ escapesBuilder.append(c);
+ }
+ }
+ PERMISSIVE_ESCAPES = escapesBuilder.toString();
+ }
+
+ public static final TextAttributesKey JSON_BRACKETS = TextAttributesKey.createTextAttributesKey("JSON.BRACKETS", BRACKETS);
+ public static final TextAttributesKey JSON_BRACES = TextAttributesKey.createTextAttributesKey("JSON.BRACES", BRACES);
+ public static final TextAttributesKey JSON_COMMA = TextAttributesKey.createTextAttributesKey("JSON.COMMA", COMMA);
+ public static final TextAttributesKey JSON_COLON = TextAttributesKey.createTextAttributesKey("JSON.COLON", SEMICOLON);
+ public static final TextAttributesKey JSON_NUMBER = TextAttributesKey.createTextAttributesKey("JSON.NUMBER", NUMBER);
+ public static final TextAttributesKey JSON_STRING = TextAttributesKey.createTextAttributesKey("JSON.STRING", STRING);
+ public static final TextAttributesKey JSON_KEYWORD = TextAttributesKey.createTextAttributesKey("JSON.KEYWORD", KEYWORD);
+ public static final TextAttributesKey JSON_LINE_COMMENT = TextAttributesKey.createTextAttributesKey("JSON.LINE_COMMENT", LINE_COMMENT);
+ public static final TextAttributesKey JSON_BLOCK_COMMENT = TextAttributesKey.createTextAttributesKey("JSON.BLOCK_COMMENT", BLOCK_COMMENT);
+
+ // Artificial element type
+ public static final TextAttributesKey JSON_IDENTIFIER = TextAttributesKey.createTextAttributesKey("JSON.IDENTIFIER", IDENTIFIER);
+
+ // Added by annotators
+ public static final TextAttributesKey JSON_PROPERTY_KEY = TextAttributesKey.createTextAttributesKey("JSON.PROPERTY_KEY", INSTANCE_FIELD);
+
+ // String escapes
+ public static final TextAttributesKey JSON_VALID_ESCAPE =
+ TextAttributesKey.createTextAttributesKey("JSON.VALID_ESCAPE", VALID_STRING_ESCAPE);
+ public static final TextAttributesKey JSON_INVALID_ESCAPE =
+ TextAttributesKey.createTextAttributesKey("JSON.INVALID_ESCAPE", INVALID_STRING_ESCAPE);
+
+
+ @NotNull
+ @Override
+ public SyntaxHighlighter getSyntaxHighlighter(@Nullable Project project, @Nullable VirtualFile virtualFile) {
+ return new MyHighlighter(virtualFile);
+ }
+
+ private class MyHighlighter extends SyntaxHighlighterBase {
+ private final Map<IElementType, TextAttributesKey> ourAttributes = new HashMap<>();
+
+ @Nullable
+ private final VirtualFile myFile;
+
+ {
+ fillMap(ourAttributes, JSON_BRACES, JsonElementTypes.L_CURLY, JsonElementTypes.R_CURLY);
+ fillMap(ourAttributes, JSON_BRACKETS, JsonElementTypes.L_BRACKET, JsonElementTypes.R_BRACKET);
+ fillMap(ourAttributes, JSON_COMMA, JsonElementTypes.COMMA);
+ fillMap(ourAttributes, JSON_COLON, JsonElementTypes.COLON);
+ fillMap(ourAttributes, JSON_STRING, JsonElementTypes.DOUBLE_QUOTED_STRING);
+ fillMap(ourAttributes, JSON_STRING, JsonElementTypes.SINGLE_QUOTED_STRING);
+ fillMap(ourAttributes, JSON_NUMBER, JsonElementTypes.NUMBER);
+ fillMap(ourAttributes, JSON_KEYWORD, JsonElementTypes.TRUE, JsonElementTypes.FALSE, JsonElementTypes.NULL);
+ fillMap(ourAttributes, JSON_LINE_COMMENT, JsonElementTypes.LINE_COMMENT);
+ fillMap(ourAttributes, JSON_BLOCK_COMMENT, JsonElementTypes.BLOCK_COMMENT);
+ // TODO may be it's worth to add more sensible highlighting for identifiers
+ fillMap(ourAttributes, JSON_IDENTIFIER, JsonElementTypes.IDENTIFIER);
+ fillMap(ourAttributes, HighlighterColors.BAD_CHARACTER, TokenType.BAD_CHARACTER);
+
+ fillMap(ourAttributes, JSON_VALID_ESCAPE, StringEscapesTokenTypes.VALID_STRING_ESCAPE_TOKEN);
+ fillMap(ourAttributes, JSON_INVALID_ESCAPE, StringEscapesTokenTypes.INVALID_CHARACTER_ESCAPE_TOKEN);
+ fillMap(ourAttributes, JSON_INVALID_ESCAPE, StringEscapesTokenTypes.INVALID_UNICODE_ESCAPE_TOKEN);
+ }
+
+ MyHighlighter(@Nullable VirtualFile file) {
+ myFile = file;
+ }
+
+ @NotNull
+ @Override
+ public Lexer getHighlightingLexer() {
+ LayeredLexer layeredLexer = new LayeredLexer(getLexer());
+ boolean isPermissiveDialect = isPermissiveDialect();
+ layeredLexer.registerSelfStoppingLayer(new StringLiteralLexer('\"', JsonElementTypes.DOUBLE_QUOTED_STRING, isCanEscapeEol(),
+ isPermissiveDialect ? PERMISSIVE_ESCAPES : "/", false, isPermissiveDialect) {
+ @NotNull
+ @Override
+ protected IElementType handleSingleSlashEscapeSequence() {
+ return isPermissiveDialect ? myOriginalLiteralToken : super.handleSingleSlashEscapeSequence();
+ }
+
+ @Override
+ protected boolean shouldAllowSlashZero() {
+ return isPermissiveDialect;
+ }
+ },
+ new IElementType[]{JsonElementTypes.DOUBLE_QUOTED_STRING}, IElementType.EMPTY_ARRAY);
+ layeredLexer.registerSelfStoppingLayer(new StringLiteralLexer('\'', JsonElementTypes.SINGLE_QUOTED_STRING, isCanEscapeEol(),
+ isPermissiveDialect ? PERMISSIVE_ESCAPES : "/", false, isPermissiveDialect){
+ @NotNull
+ @Override
+ protected IElementType handleSingleSlashEscapeSequence() {
+ return isPermissiveDialect ? myOriginalLiteralToken : super.handleSingleSlashEscapeSequence();
+ }
+
+ @Override
+ protected boolean shouldAllowSlashZero() {
+ return isPermissiveDialect;
+ }
+ },
+ new IElementType[]{JsonElementTypes.SINGLE_QUOTED_STRING}, IElementType.EMPTY_ARRAY);
+ return layeredLexer;
+ }
+
+ private boolean isPermissiveDialect() {
+ FileType fileType = myFile == null ? null : myFile.getFileType();
+ boolean isPermissiveDialect = false;
+ if (fileType instanceof JsonFileType) {
+ Language language = ((JsonFileType)fileType).getLanguage();
+ isPermissiveDialect = language instanceof JsonLanguage && ((JsonLanguage)language).hasPermissiveStrings();
+ }
+ return isPermissiveDialect;
+ }
+
+ @NotNull
+ @Override
+ public TextAttributesKey[] getTokenHighlights(IElementType type) {
+ return pack(ourAttributes.get(type));
+ }
+ }
+
+ @NotNull
+ protected Lexer getLexer() {
+ return new JsonLexer();
+ }
+
+ protected boolean isCanEscapeEol() {
+ return false;
+ }
+}
diff --git a/json/src/com/intellij/json/json5/Json5FileType.java b/json/src/com/intellij/json/json5/Json5FileType.java
new file mode 100644
index 00000000..b9979faf
--- /dev/null
+++ b/json/src/com/intellij/json/json5/Json5FileType.java
@@ -0,0 +1,32 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5;
+
+import com.intellij.json.JsonFileType;
+import org.jetbrains.annotations.NotNull;
+
+public class Json5FileType extends JsonFileType {
+ public static final Json5FileType INSTANCE = new Json5FileType();
+ public static final String DEFAULT_EXTENSION = "json5";
+
+ public Json5FileType() {
+ super(Json5Language.INSTANCE);
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return "JSON5";
+ }
+
+ @NotNull
+ @Override
+ public String getDescription() {
+ return "JSON5";
+ }
+
+ @NotNull
+ @Override
+ public String getDefaultExtension() {
+ return DEFAULT_EXTENSION;
+ }
+}
diff --git a/json/src/com/intellij/json/json5/Json5FileTypeFactory.java b/json/src/com/intellij/json/json5/Json5FileTypeFactory.java
new file mode 100644
index 00000000..bf6a0254
--- /dev/null
+++ b/json/src/com/intellij/json/json5/Json5FileTypeFactory.java
@@ -0,0 +1,13 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5;
+
+import com.intellij.openapi.fileTypes.FileTypeConsumer;
+import com.intellij.openapi.fileTypes.FileTypeFactory;
+import org.jetbrains.annotations.NotNull;
+
+public class Json5FileTypeFactory extends FileTypeFactory {
+ @Override
+ public void createFileTypes(@NotNull FileTypeConsumer consumer) {
+ consumer.consume(Json5FileType.INSTANCE, Json5FileType.DEFAULT_EXTENSION);
+ }
+}
diff --git a/json/src/com/intellij/json/json5/Json5Language.java b/json/src/com/intellij/json/json5/Json5Language.java
new file mode 100644
index 00000000..a6412639
--- /dev/null
+++ b/json/src/com/intellij/json/json5/Json5Language.java
@@ -0,0 +1,17 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5;
+
+import com.intellij.json.JsonLanguage;
+
+public class Json5Language extends JsonLanguage {
+ public static final Json5Language INSTANCE = new Json5Language();
+
+ protected Json5Language() {
+ super("JSON5", "application/json5");
+ }
+
+ @Override
+ public boolean hasPermissiveStrings() {
+ return true;
+ }
+}
diff --git a/json/src/com/intellij/json/json5/Json5Lexer.java b/json/src/com/intellij/json/json5/Json5Lexer.java
new file mode 100644
index 00000000..0617e379
--- /dev/null
+++ b/json/src/com/intellij/json/json5/Json5Lexer.java
@@ -0,0 +1,10 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5;
+
+import com.intellij.lexer.FlexAdapter;
+
+public class Json5Lexer extends FlexAdapter {
+ public Json5Lexer() {
+ super(new _Json5Lexer());
+ }
+}
diff --git a/json/src/com/intellij/json/json5/Json5ParserDefinition.java b/json/src/com/intellij/json/json5/Json5ParserDefinition.java
new file mode 100644
index 00000000..d0b146bc
--- /dev/null
+++ b/json/src/com/intellij/json/json5/Json5ParserDefinition.java
@@ -0,0 +1,31 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5;
+
+import com.intellij.json.JsonParserDefinition;
+import com.intellij.json.psi.impl.JsonFileImpl;
+import com.intellij.lexer.Lexer;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.tree.IFileElementType;
+import org.jetbrains.annotations.NotNull;
+
+public class Json5ParserDefinition extends JsonParserDefinition {
+ public static final IFileElementType FILE = new IFileElementType(Json5Language.INSTANCE);
+
+ @NotNull
+ @Override
+ public Lexer createLexer(Project project) {
+ return new Json5Lexer();
+ }
+
+ @Override
+ public PsiFile createFile(FileViewProvider fileViewProvider) {
+ return new JsonFileImpl(fileViewProvider, Json5Language.INSTANCE);
+ }
+
+ @Override
+ public IFileElementType getFileNodeType() {
+ return FILE;
+ }
+}
diff --git a/json/src/com/intellij/json/json5/Json5PsiWalkerFactory.java b/json/src/com/intellij/json/json5/Json5PsiWalkerFactory.java
new file mode 100644
index 00000000..6ba518b4
--- /dev/null
+++ b/json/src/com/intellij/json/json5/Json5PsiWalkerFactory.java
@@ -0,0 +1,36 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5;
+
+import com.intellij.codeInsight.completion.CompletionUtil;
+import com.intellij.json.JsonDialectUtil;
+import com.intellij.psi.PsiElement;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalkerFactory;
+import com.jetbrains.jsonSchema.impl.JsonOriginalPsiWalker;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import org.jetbrains.annotations.NotNull;
+
+public class Json5PsiWalkerFactory implements JsonLikePsiWalkerFactory {
+ @Override
+ public boolean handles(@NotNull PsiElement element) {
+ PsiElement parent = element.getParent();
+ if (parent == null) return false;
+ return JsonDialectUtil.getLanguage(CompletionUtil.getOriginalOrSelf(parent)) == Json5Language.INSTANCE;
+ }
+
+ @NotNull
+ @Override
+ public JsonLikePsiWalker create(@NotNull JsonSchemaObject schemaObject) {
+ return new JsonOriginalPsiWalker() {
+ @Override
+ public boolean isNameQuoted() {
+ return false;
+ }
+
+ @Override
+ public boolean onlyDoubleQuotesForStringLiterals() {
+ return false;
+ }
+ };
+ }
+}
diff --git a/json/src/com/intellij/json/json5/_Json5Lexer.flex b/json/src/com/intellij/json/json5/_Json5Lexer.flex
new file mode 100644
index 00000000..14f4130f
--- /dev/null
+++ b/json/src/com/intellij/json/json5/_Json5Lexer.flex
@@ -0,0 +1,64 @@
+package com.intellij.json.json5;
+
+import com.intellij.lexer.FlexLexer;
+import com.intellij.psi.tree.IElementType;
+
+import static com.intellij.psi.TokenType.BAD_CHARACTER;
+import static com.intellij.psi.TokenType.WHITE_SPACE;
+import static com.intellij.json.JsonElementTypes.*;
+
+%%
+
+%{
+ public _Json5Lexer() {
+ this((java.io.Reader)null);
+ }
+%}
+
+%public
+%class _Json5Lexer
+%implements FlexLexer
+%function advance
+%type IElementType
+%unicode
+
+EOL=\R
+WHITE_SPACE=\s+
+HEX_DIGIT=[0-9A-Fa-f]
+
+LINE_COMMENT="//".*
+BLOCK_COMMENT="/"\*([^*]|\*+[^*/])*(\*+"/")?
+LINE_TERMINATOR_SEQUENCE=\R
+CRLF= [\ \t \f]* {LINE_TERMINATOR_SEQUENCE}
+DOUBLE_QUOTED_STRING=\"([^\\\"\r\n]|\\[^\r\n]|\\{CRLF})*\"?
+SINGLE_QUOTED_STRING='([^\\'\r\n]|\\[^\r\n]|\\{CRLF})*'?
+JSON5_NUMBER=(\+|-)?(0|[1-9][0-9]*)?\.?([0-9]+)?([eE][+-]?[0-9]*)?
+HEX_DIGITS=({HEX_DIGIT})+
+HEX_INTEGER_LITERAL=(\+|-)?0[Xx]({HEX_DIGITS})
+NUMBER={JSON5_NUMBER}|{HEX_INTEGER_LITERAL}|Infinity|-Infinity|\+Infinity|NaN|-NaN|\+NaN
+IDENTIFIER=[[:jletterdigit:]~!()*\-."/"@\^<>=]+
+
+%%
+<YYINITIAL> {
+ {WHITE_SPACE} { return WHITE_SPACE; }
+
+ "{" { return L_CURLY; }
+ "}" { return R_CURLY; }
+ "[" { return L_BRACKET; }
+ "]" { return R_BRACKET; }
+ "," { return COMMA; }
+ ":" { return COLON; }
+ "true" { return TRUE; }
+ "false" { return FALSE; }
+ "null" { return NULL; }
+
+ {LINE_COMMENT} { return LINE_COMMENT; }
+ {BLOCK_COMMENT} { return BLOCK_COMMENT; }
+ {DOUBLE_QUOTED_STRING} { return DOUBLE_QUOTED_STRING; }
+ {SINGLE_QUOTED_STRING} { return SINGLE_QUOTED_STRING; }
+ {NUMBER} { return NUMBER; }
+ {IDENTIFIER} { return IDENTIFIER; }
+
+}
+
+[^] { return BAD_CHARACTER; }
diff --git a/json/src/com/intellij/json/json5/codeinsight/Json5JsonLiteralChecker.java b/json/src/com/intellij/json/json5/codeinsight/Json5JsonLiteralChecker.java
new file mode 100644
index 00000000..1ed1f273
--- /dev/null
+++ b/json/src/com/intellij/json/json5/codeinsight/Json5JsonLiteralChecker.java
@@ -0,0 +1,52 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5.codeinsight;
+
+import com.intellij.json.JsonDialectUtil;
+import com.intellij.json.codeinsight.JsonLiteralChecker;
+import com.intellij.json.codeinsight.StandardJsonLiteralChecker;
+import com.intellij.json.json5.Json5Language;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.regex.Pattern;
+
+public class Json5JsonLiteralChecker implements JsonLiteralChecker {
+ private static final Pattern VALID_HEX_ESCAPE = Pattern.compile("\\\\(x[0-9a-fA-F]{2})");
+ private static final Pattern INVALID_NUMERIC_ESCAPE = Pattern.compile("\\\\[1-9]");
+ @Nullable
+ @Override
+ public String getErrorForNumericLiteral(String literalText) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Pair<TextRange, String> getErrorForStringFragment(Pair<TextRange, String> fragment, JsonStringLiteral stringLiteral) {
+ String fragmentText = fragment.second;
+ if (fragmentText.startsWith("\\") && fragmentText.length() > 1 && fragmentText.endsWith("\n")) {
+ if (StringUtil.isEmptyOrSpaces(fragmentText.substring(1, fragmentText.length() - 1))) {
+ return null;
+ }
+ }
+
+ if (fragmentText.startsWith("\\x") && VALID_HEX_ESCAPE.matcher(fragmentText).matches()) {
+ return null;
+ }
+
+ if (!StandardJsonLiteralChecker.VALID_ESCAPE.matcher(fragmentText).matches() && !INVALID_NUMERIC_ESCAPE.matcher(fragmentText).matches()) {
+ return null;
+ }
+
+ final String error = StandardJsonLiteralChecker.getStringError(fragmentText);
+ return error == null ? null : Pair.create(fragment.first, error);
+ }
+
+ @Override
+ public boolean isApplicable(PsiElement element) {
+ return JsonDialectUtil.getLanguage(element) == Json5Language.INSTANCE;
+ }
+}
diff --git a/json/src/com/intellij/json/json5/codeinsight/Json5StandardComplianceInspection.java b/json/src/com/intellij/json/json5/codeinsight/Json5StandardComplianceInspection.java
new file mode 100644
index 00000000..d61cffa1
--- /dev/null
+++ b/json/src/com/intellij/json/json5/codeinsight/Json5StandardComplianceInspection.java
@@ -0,0 +1,81 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5.codeinsight;
+
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.json.JsonBundle;
+import com.intellij.json.JsonDialectUtil;
+import com.intellij.json.codeinsight.JsonStandardComplianceInspection;
+import com.intellij.json.json5.Json5Language;
+import com.intellij.json.psi.JsonLiteral;
+import com.intellij.json.psi.JsonPsiUtil;
+import com.intellij.json.psi.JsonReferenceExpression;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+
+public class Json5StandardComplianceInspection extends JsonStandardComplianceInspection {
+
+ @Override
+ @NotNull
+ public String getDisplayName() {
+ return JsonBundle.message("inspection.compliance5.name");
+ }
+
+ @NotNull
+ @Override
+ public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
+ if (!(JsonDialectUtil.getLanguage(holder.getFile()) instanceof Json5Language)) return PsiElementVisitor.EMPTY_VISITOR;
+ return new StandardJson5ValidatingElementVisitor(holder);
+ }
+
+ @Override
+ public JComponent createOptionsPanel() {
+ return null;
+ }
+
+ private class StandardJson5ValidatingElementVisitor extends StandardJsonValidatingElementVisitor {
+ StandardJson5ValidatingElementVisitor(ProblemsHolder holder) {
+ super(holder);
+ }
+
+ @Override
+ protected boolean allowComments() {
+ return true;
+ }
+
+ @Override
+ protected boolean allowSingleQuotes() {
+ return true;
+ }
+
+ @Override
+ protected boolean allowIdentifierPropertyNames() {
+ return true;
+ }
+
+ @Override
+ protected boolean allowTrailingCommas() {
+ return true;
+ }
+
+ @Override
+ protected boolean allowNanInfinity() {
+ return true;
+ }
+
+ @Override
+ protected boolean isValidPropertyName(@NotNull PsiElement literal) {
+ if (literal instanceof JsonLiteral) {
+ String textWithoutHostEscaping = JsonPsiUtil.getElementTextWithoutHostEscaping(literal);
+ return textWithoutHostEscaping.startsWith("\"") || textWithoutHostEscaping.startsWith("'");
+ }
+ if (literal instanceof JsonReferenceExpression) {
+ return StringUtil.isJavaIdentifier(literal.getText());
+ }
+ return false;
+ }
+ }
+}
diff --git a/json/src/com/intellij/json/json5/highlighting/Json5SyntaxHighlightingFactory.java b/json/src/com/intellij/json/json5/highlighting/Json5SyntaxHighlightingFactory.java
new file mode 100644
index 00000000..f20466f5
--- /dev/null
+++ b/json/src/com/intellij/json/json5/highlighting/Json5SyntaxHighlightingFactory.java
@@ -0,0 +1,20 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.json5.highlighting;
+
+import com.intellij.json.highlighting.JsonSyntaxHighlighterFactory;
+import com.intellij.json.json5.Json5Lexer;
+import com.intellij.lexer.Lexer;
+import org.jetbrains.annotations.NotNull;
+
+public class Json5SyntaxHighlightingFactory extends JsonSyntaxHighlighterFactory {
+ @NotNull
+ @Override
+ protected Lexer getLexer() {
+ return new Json5Lexer();
+ }
+
+ @Override
+ protected boolean isCanEscapeEol() {
+ return true;
+ }
+}
diff --git a/json/src/com/intellij/json/liveTemplates/JsonContextType.java b/json/src/com/intellij/json/liveTemplates/JsonContextType.java
new file mode 100644
index 00000000..cc5ef222
--- /dev/null
+++ b/json/src/com/intellij/json/liveTemplates/JsonContextType.java
@@ -0,0 +1,22 @@
+package com.intellij.json.liveTemplates;
+
+import com.intellij.codeInsight.template.FileTypeBasedContextType;
+import com.intellij.json.JsonBundle;
+import com.intellij.json.JsonFileType;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Konstantin.Ulitin
+ */
+public class JsonContextType extends FileTypeBasedContextType {
+ protected JsonContextType() {
+ super("JSON", JsonBundle.message("json.template.context.type"), JsonFileType.INSTANCE);
+ }
+
+ @Override
+ public boolean isInContext(@NotNull PsiFile file, int offset) {
+ return file instanceof JsonFile;
+ }
+}
diff --git a/json/src/com/intellij/json/liveTemplates/JsonInLiteralsContextType.java b/json/src/com/intellij/json/liveTemplates/JsonInLiteralsContextType.java
new file mode 100644
index 00000000..0e21d5fa
--- /dev/null
+++ b/json/src/com/intellij/json/liveTemplates/JsonInLiteralsContextType.java
@@ -0,0 +1,21 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.liveTemplates;
+
+import com.intellij.codeInsight.template.TemplateContextType;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+
+import static com.intellij.patterns.PlatformPatterns.psiElement;
+
+public class JsonInLiteralsContextType extends TemplateContextType {
+ protected JsonInLiteralsContextType() {
+ super("JSON_STRING_VALUES", "JSON String Values", JsonContextType.class);
+ }
+
+ @Override
+ public boolean isInContext(@NotNull PsiFile file, int offset) {
+ return file instanceof JsonFile && psiElement().inside(JsonStringLiteral.class).accepts(file.findElementAt(offset));
+ }
+}
diff --git a/json/src/com/intellij/json/liveTemplates/JsonInPropertyKeysContextType.java b/json/src/com/intellij/json/liveTemplates/JsonInPropertyKeysContextType.java
new file mode 100644
index 00000000..145e1b28
--- /dev/null
+++ b/json/src/com/intellij/json/liveTemplates/JsonInPropertyKeysContextType.java
@@ -0,0 +1,33 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.liveTemplates;
+
+import com.intellij.codeInsight.template.TemplateContextType;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.json.psi.JsonPsiUtil;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.patterns.PatternCondition;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.util.ProcessingContext;
+import org.jetbrains.annotations.NotNull;
+
+import static com.intellij.patterns.PlatformPatterns.psiElement;
+
+public class JsonInPropertyKeysContextType extends TemplateContextType {
+ protected JsonInPropertyKeysContextType() {
+ super("JSON_PROPERTY_KEYS", "JSON Property Keys", JsonContextType.class);
+ }
+
+ @Override
+ public boolean isInContext(@NotNull PsiFile file, int offset) {
+ return file instanceof JsonFile && psiElement().inside(psiElement(JsonValue.class)
+ .with(new PatternCondition<PsiElement>("insidePropertyKey") {
+ @Override
+ public boolean accepts(@NotNull PsiElement element,
+ ProcessingContext context) {
+ return JsonPsiUtil.isPropertyKey(element);
+ }
+ })).beforeLeaf(psiElement(JsonElementTypes.COLON)).accepts(file.findElementAt(offset));
+ }
+} \ No newline at end of file
diff --git a/json/src/com/intellij/json/navigation/JsonQualifiedNameKind.java b/json/src/com/intellij/json/navigation/JsonQualifiedNameKind.java
new file mode 100644
index 00000000..f82115fc
--- /dev/null
+++ b/json/src/com/intellij/json/navigation/JsonQualifiedNameKind.java
@@ -0,0 +1,20 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.navigation;
+
+import kotlin.NotImplementedError;
+
+public enum JsonQualifiedNameKind {
+ Qualified,
+ JsonPointer;
+
+ @Override
+ public String toString() {
+ switch (this) {
+ case Qualified:
+ return "qualified name";
+ case JsonPointer:
+ return "JSON Pointer";
+ }
+ throw new NotImplementedError("Unknown name kind: " + this.name());
+ }
+}
diff --git a/json/src/com/intellij/json/navigation/JsonQualifiedNameProvider.java b/json/src/com/intellij/json/navigation/JsonQualifiedNameProvider.java
new file mode 100644
index 00000000..31ee728c
--- /dev/null
+++ b/json/src/com/intellij/json/navigation/JsonQualifiedNameProvider.java
@@ -0,0 +1,69 @@
+package com.intellij.json.navigation;
+
+import com.intellij.ide.actions.QualifiedNameProvider;
+import com.intellij.json.JsonUtil;
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonElement;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.jetbrains.jsonSchema.JsonPointerUtil;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonQualifiedNameProvider implements QualifiedNameProvider {
+ @Nullable
+ @Override
+ public PsiElement adjustElementToCopy(PsiElement element) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getQualifiedName(PsiElement element) {
+ return generateQualifiedName(element, JsonQualifiedNameKind.Qualified);
+ }
+
+ public static String generateQualifiedName(PsiElement element, JsonQualifiedNameKind qualifiedNameKind) {
+ if (!(element instanceof JsonElement)) {
+ return null;
+ }
+ JsonElement parentProperty = PsiTreeUtil.getNonStrictParentOfType(element, JsonProperty.class, JsonArray.class);
+ StringBuilder builder = new StringBuilder();
+ while (parentProperty != null) {
+ if (parentProperty instanceof JsonProperty) {
+ String name = parentProperty.getName();
+ if (qualifiedNameKind == JsonQualifiedNameKind.JsonPointer) {
+ name = name == null ? null : JsonPointerUtil.escapeForJsonPointer(name);
+ }
+ builder.insert(0, name);
+ builder.insert(0, qualifiedNameKind == JsonQualifiedNameKind.JsonPointer ? "/" : ".");
+ }
+ else {
+ int index = JsonUtil.getArrayIndexOfItem(element instanceof JsonProperty ? element.getParent() : element);
+ if (index == -1) return null;
+ builder.insert(0, qualifiedNameKind == JsonQualifiedNameKind.JsonPointer ? ("/" + index) : ("[" + index + "]"));
+ }
+ element = parentProperty;
+ parentProperty = PsiTreeUtil.getParentOfType(parentProperty, JsonProperty.class, JsonArray.class);
+ }
+
+ if (builder.length() == 0) return null;
+
+ // if the first operation is array indexing, we insert the 'root' element $
+ if (builder.charAt(0) == '[') {
+ builder.insert(0, "$");
+ }
+
+ return StringUtil.trimStart(builder.toString(), ".");
+ }
+
+ @Override
+ public PsiElement qualifiedNameToElement(String fqn, Project project) {
+ return null;
+ }
+}
diff --git a/json/src/com/intellij/json/psi/JsonElement.java b/json/src/com/intellij/json/psi/JsonElement.java
new file mode 100644
index 00000000..f3345255
--- /dev/null
+++ b/json/src/com/intellij/json/psi/JsonElement.java
@@ -0,0 +1,10 @@
+package com.intellij.json.psi;
+
+import com.intellij.psi.NavigatablePsiElement;
+import com.intellij.psi.PsiElement;
+
+/**
+ * @author Mikhail Golubev
+ */
+public interface JsonElement extends PsiElement, NavigatablePsiElement {
+}
diff --git a/json/src/com/intellij/json/psi/JsonElementGenerator.java b/json/src/com/intellij/json/psi/JsonElementGenerator.java
new file mode 100644
index 00000000..535f9d58
--- /dev/null
+++ b/json/src/com/intellij/json/psi/JsonElementGenerator.java
@@ -0,0 +1,79 @@
+package com.intellij.json.psi;
+
+import com.intellij.json.JsonFileType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiFileFactory;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonElementGenerator {
+ private final Project myProject;
+
+ public JsonElementGenerator(@NotNull Project project) {
+ myProject = project;
+ }
+
+ /**
+ * Create lightweight in-memory {@link com.intellij.json.psi.JsonFile} filled with {@code content}.
+ *
+ * @param content content of the file to be created
+ * @return created file
+ */
+ @NotNull
+ public PsiFile createDummyFile(@NotNull String content) {
+ final PsiFileFactory psiFileFactory = PsiFileFactory.getInstance(myProject);
+ return psiFileFactory.createFileFromText("dummy." + JsonFileType.INSTANCE.getDefaultExtension(), JsonFileType.INSTANCE, content);
+ }
+
+ /**
+ * Create JSON value from supplied content.
+ *
+ * @param content properly escaped text of JSON value, e.g. Java literal {@code "\"new\\nline\""} if you want to create string literal
+ * @param <T> type of the JSON value desired
+ * @return element created from given text
+ *
+ * @see #createStringLiteral(String)
+ */
+ @NotNull
+ public <T extends JsonValue> T createValue(@NotNull String content) {
+ final PsiFile file = createDummyFile("{\"foo\": " + content + "}");
+ //noinspection unchecked,ConstantConditions
+ return (T)((JsonObject)file.getFirstChild()).getPropertyList().get(0).getValue();
+ }
+
+ @NotNull
+ public JsonObject createObject(@NotNull String content) {
+ final PsiFile file = createDummyFile("{" + content + "}");
+ // noinspection ConstantConditions
+ return (JsonObject) file.getFirstChild();
+ }
+
+ /**
+ * Create JSON string literal from supplied <em>unescaped</em> content.
+ *
+ * @param unescapedContent unescaped content of string literal, e.g. Java literal {@code "new\nline"} (compare with {@link #createValue(String)}).
+ * @return JSON string literal created from given text
+ */
+ @NotNull
+ public JsonStringLiteral createStringLiteral(@NotNull String unescapedContent) {
+ return createValue('"' + StringUtil.escapeStringCharacters(unescapedContent) + '"');
+ }
+
+ @NotNull
+ public JsonProperty createProperty(@NotNull final String name, @NotNull final String value) {
+ final PsiFile file = createDummyFile("{\"" + name + "\": " + value + "}");
+ // noinspection ConstantConditions
+ return ((JsonObject) file.getFirstChild()).getPropertyList().get(0);
+ }
+
+ @NotNull
+ public PsiElement createComma() {
+ final JsonArray jsonArray1 = createValue("[1, 2]");
+ return jsonArray1.getValueList().get(0).getNextSibling();
+ }
+}
diff --git a/json/src/com/intellij/json/psi/JsonFile.java b/json/src/com/intellij/json/psi/JsonFile.java
new file mode 100644
index 00000000..25fb4829
--- /dev/null
+++ b/json/src/com/intellij/json/psi/JsonFile.java
@@ -0,0 +1,23 @@
+package com.intellij.json.psi;
+
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public interface JsonFile extends JsonElement, PsiFile {
+ /**
+ * Returns {@link JsonArray} or {@link JsonObject} value according to JSON standard.
+ *
+ * @return top-level JSON element if any or {@code null} otherwise
+ */
+ @Nullable
+ JsonValue getTopLevelValue();
+
+ @NotNull
+ List<JsonValue> getAllTopLevelValues();
+}
diff --git a/json/src/com/intellij/json/psi/JsonParserUtil.java b/json/src/com/intellij/json/psi/JsonParserUtil.java
new file mode 100644
index 00000000..d0f5f467
--- /dev/null
+++ b/json/src/com/intellij/json/psi/JsonParserUtil.java
@@ -0,0 +1,25 @@
+package com.intellij.json.psi;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.lang.PsiBuilder;
+import com.intellij.lang.parser.GeneratedParserUtilBase;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonParserUtil extends GeneratedParserUtilBase {
+
+ public static boolean notTrailingComma(@NotNull PsiBuilder builder, int level) {
+ if (builder.getTokenType() != JsonElementTypes.COMMA) {
+ return false;
+ }
+ final IElementType afterComma = builder.lookAhead(1);
+ if (afterComma == JsonElementTypes.R_BRACKET || afterComma == JsonElementTypes.R_CURLY) {
+ builder.error("trailing comma");
+ }
+ builder.advanceLexer();
+ return true;
+ }
+}
diff --git a/json/src/com/intellij/json/psi/JsonPsiChangeUtils.java b/json/src/com/intellij/json/psi/JsonPsiChangeUtils.java
new file mode 100644
index 00000000..4c4a9385
--- /dev/null
+++ b/json/src/com/intellij/json/psi/JsonPsiChangeUtils.java
@@ -0,0 +1,42 @@
+package com.intellij.json.psi;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.TokenType;
+
+public class JsonPsiChangeUtils {
+ public static void removeCommaSeparatedFromList(final ASTNode myNode, final ASTNode parent) {
+ ASTNode from = myNode, to = myNode.getTreeNext();
+
+ boolean seenComma = false;
+
+ ASTNode toCandidate = to;
+ while (toCandidate != null && toCandidate.getElementType() == TokenType.WHITE_SPACE) {
+ toCandidate = toCandidate.getTreeNext();
+ }
+
+ if (toCandidate != null && toCandidate.getElementType() == JsonElementTypes.COMMA) {
+ toCandidate = toCandidate.getTreeNext();
+ to = toCandidate;
+ seenComma = true;
+
+ if (to != null && to.getElementType() == TokenType.WHITE_SPACE) {
+ to = to.getTreeNext();
+ }
+ }
+
+ if (!seenComma) {
+ ASTNode treePrev = from.getTreePrev();
+
+ while (treePrev != null && treePrev.getElementType() == TokenType.WHITE_SPACE) {
+ from = treePrev;
+ treePrev = treePrev.getTreePrev();
+ }
+ if (treePrev != null && treePrev.getElementType() == JsonElementTypes.COMMA) {
+ from = treePrev;
+ }
+ }
+
+ parent.removeRange(from, to);
+ }
+}
diff --git a/json/src/com/intellij/json/psi/JsonPsiUtil.java b/json/src/com/intellij/json/psi/JsonPsiUtil.java
new file mode 100644
index 00000000..78cf5b34
--- /dev/null
+++ b/json/src/com/intellij/json/psi/JsonPsiUtil.java
@@ -0,0 +1,231 @@
+package com.intellij.json.psi;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.injection.InjectedLanguageManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.TokenType;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.tree.TokenSet;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import static com.intellij.json.JsonParserDefinition.JSON_COMMENTARIES;
+
+/**
+ * Various helper methods for working with PSI of JSON language.
+ *
+ * @author Mikhail Golubev
+ */
+@SuppressWarnings("UnusedDeclaration")
+public class JsonPsiUtil {
+ private JsonPsiUtil() {
+ // empty
+ }
+
+
+ /**
+ * Checks that PSI element represents item of JSON array.
+ *
+ * @param element PSI element to check
+ * @return whether this PSI element is array element
+ */
+ public static boolean isArrayElement(@NotNull PsiElement element) {
+ return element instanceof JsonValue && element.getParent() instanceof JsonArray;
+ }
+
+ /**
+ * Checks that PSI element represents key of JSON property (key-value pair of JSON object)
+ *
+ * @param element PSI element to check
+ * @return whether this PSI element is property key
+ */
+ public static boolean isPropertyKey(@NotNull PsiElement element) {
+ final PsiElement parent = element.getParent();
+ return parent instanceof JsonProperty && element == ((JsonProperty)parent).getNameElement();
+ }
+
+ /**
+ * Checks that PSI element represents value of JSON property (key-value pair of JSON object)
+ *
+ * @param element PSI element to check
+ * @return whether this PSI element is property value
+ */
+ public static boolean isPropertyValue(@NotNull PsiElement element) {
+ final PsiElement parent = element.getParent();
+ return parent instanceof JsonProperty && element == ((JsonProperty)parent).getValue();
+ }
+
+ /**
+ * Find the furthest sibling element with the same type as given anchor.
+ * <p/>
+ * Ignore white spaces for any type of element except {@link com.intellij.json.JsonElementTypes#LINE_COMMENT}
+ * where non indentation white space (that has new line in the middle) will stop the search.
+ *
+ * @param anchor element to start from
+ * @param after whether to scan through sibling elements forward or backward
+ * @return described element or anchor if search stops immediately
+ */
+ @NotNull
+ public static PsiElement findFurthestSiblingOfSameType(@NotNull PsiElement anchor, boolean after) {
+ ASTNode node = anchor.getNode();
+ // Compare by node type to distinguish between different types of comments
+ final IElementType expectedType = node.getElementType();
+ ASTNode lastSeen = node;
+ while (node != null) {
+ final IElementType elementType = node.getElementType();
+ if (elementType == expectedType) {
+ lastSeen = node;
+ }
+ else if (elementType == TokenType.WHITE_SPACE) {
+ if (expectedType == JsonElementTypes.LINE_COMMENT && node.getText().indexOf('\n', 1) != -1) {
+ break;
+ }
+ }
+ else if (!JSON_COMMENTARIES.contains(elementType) || JSON_COMMENTARIES.contains(expectedType)) {
+ break;
+ }
+ node = after ? node.getTreeNext() : node.getTreePrev();
+ }
+ return lastSeen.getPsi();
+ }
+
+ /**
+ * Check that element type of the given AST node belongs to the token set.
+ * <p/>
+ * It slightly less verbose than {@code set.contains(node.getElementType())} and overloaded methods with the same name
+ * allow check ASTNode/PsiElement against both concrete element types and token sets in uniform way.
+ */
+ public static boolean hasElementType(@NotNull ASTNode node, @NotNull TokenSet set) {
+ return set.contains(node.getElementType());
+ }
+
+ /**
+ * @see #hasElementType(com.intellij.lang.ASTNode, com.intellij.psi.tree.TokenSet)
+ */
+ public static boolean hasElementType(@NotNull ASTNode node, IElementType... types) {
+ return hasElementType(node, TokenSet.create(types));
+ }
+
+ /**
+ * @see #hasElementType(com.intellij.lang.ASTNode, com.intellij.psi.tree.TokenSet)
+ */
+ public static boolean hasElementType(@NotNull PsiElement element, @NotNull TokenSet set) {
+ return element.getNode() != null && hasElementType(element.getNode(), set);
+ }
+
+ /**
+ * @see #hasElementType(com.intellij.lang.ASTNode, com.intellij.psi.tree.IElementType...)
+ */
+ public static boolean hasElementType(@NotNull PsiElement element, IElementType... types) {
+ return element.getNode() != null && hasElementType(element.getNode(), types);
+ }
+
+ /**
+ * Returns text of the given PSI element. Unlike obvious {@link PsiElement#getText()} this method unescapes text of the element if latter
+ * belongs to injected code fragment using {@link InjectedLanguageManager#getUnescapedText(PsiElement)}.
+ *
+ * @param element PSI element which text is needed
+ * @return text of the element with any host escaping removed
+ */
+ @NotNull
+ public static String getElementTextWithoutHostEscaping(@NotNull PsiElement element) {
+ final InjectedLanguageManager manager = InjectedLanguageManager.getInstance(element.getProject());
+ if (manager.isInjectedFragment(element.getContainingFile())) {
+ return manager.getUnescapedText(element);
+ }
+ else {
+ return element.getText();
+ }
+ }
+
+ /**
+ * Returns content of the string literal (without escaping) striving to preserve as much of user data as possible.
+ * <ul>
+ * <li>If literal length is greater than one and it starts and ends with the same quote and the last quote is not escaped, returns
+ * text without first and last characters.</li>
+ * <li>Otherwise if literal still begins with a quote, returns text without first character only.</li>
+ * <li>Returns unmodified text in all other cases.</li>
+ * </ul>
+ *
+ * @param text presumably result of {@link JsonStringLiteral#getText()}
+ * @return
+ */
+ @NotNull
+ public static String stripQuotes(@NotNull String text) {
+ if (text.length() > 0) {
+ final char firstChar = text.charAt(0);
+ final char lastChar = text.charAt(text.length() - 1);
+ if (firstChar == '\'' || firstChar == '"') {
+ if (text.length() > 1 && firstChar == lastChar && !isEscapedChar(text, text.length() - 1)) {
+ return text.substring(1, text.length() - 1);
+ }
+ return text.substring(1);
+ }
+ }
+ return text;
+ }
+
+ /**
+ * Checks that character in given position is escaped with backslashes.
+ *
+ * @param text text character belongs to
+ * @param position position of the character
+ * @return whether character at given position is escaped, i.e. preceded by odd number of backslashes
+ */
+ public static boolean isEscapedChar(@NotNull String text, int position) {
+ int count = 0;
+ for (int i = position - 1; i >= 0 && text.charAt(i) == '\\'; i--) {
+ count++;
+ }
+ return count % 2 != 0;
+ }
+
+ /**
+ * Add new property and necessary comma either at the beginning of the object literal or at its end.
+ *
+ * @param object object literal
+ * @param property new property, probably created via {@link JsonElementGenerator}
+ * @param first if true make new property first in the object, otherwise append in the end of property list
+ * @return property as returned by {@link PsiElement#addAfter(PsiElement, PsiElement)}
+ */
+ @NotNull
+ public static PsiElement addProperty(@NotNull JsonObject object, @NotNull JsonProperty property, boolean first) {
+ final List<JsonProperty> propertyList = object.getPropertyList();
+ if (!first) {
+ final JsonProperty lastProperty = ContainerUtil.getLastItem(propertyList);
+ if (lastProperty != null) {
+ final PsiElement addedProperty = object.addAfter(property, lastProperty);
+ object.addBefore(new JsonElementGenerator(object.getProject()).createComma(), addedProperty);
+ return addedProperty;
+ }
+ }
+ final PsiElement leftBrace = object.getFirstChild();
+ assert hasElementType(leftBrace, JsonElementTypes.L_CURLY);
+ final PsiElement addedProperty = object.addAfter(property, leftBrace);
+ if (!propertyList.isEmpty()) {
+ object.addAfter(new JsonElementGenerator(object.getProject()).createComma(), addedProperty);
+ }
+ return addedProperty;
+ }
+
+ @NotNull
+ public static Set<String> getOtherSiblingPropertyNames(@Nullable JsonProperty property) {
+ if (property == null) return Collections.emptySet();
+ JsonObject object = ObjectUtils.tryCast(property.getParent(), JsonObject.class);
+ if (object == null) return Collections.emptySet();
+ Set<String> result = ContainerUtil.newHashSet();
+ for (JsonProperty jsonProperty : object.getPropertyList()) {
+ if (jsonProperty != property) {
+ result.add(jsonProperty.getName());
+ }
+ }
+ return result;
+ }
+}
diff --git a/json/src/com/intellij/json/psi/JsonStringLiteralManipulator.java b/json/src/com/intellij/json/psi/JsonStringLiteralManipulator.java
new file mode 100644
index 00000000..855655f7
--- /dev/null
+++ b/json/src/com/intellij/json/psi/JsonStringLiteralManipulator.java
@@ -0,0 +1,32 @@
+package com.intellij.json.psi;
+
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.AbstractElementManipulator;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonStringLiteralManipulator extends AbstractElementManipulator<JsonStringLiteral> {
+
+ @Override
+ public JsonStringLiteral handleContentChange(@NotNull JsonStringLiteral element, @NotNull TextRange range, String newContent)
+ throws IncorrectOperationException {
+ assert new TextRange(0, element.getTextLength()).contains(range);
+
+ final String originalContent = element.getText();
+ final TextRange withoutQuotes = getRangeInElement(element);
+ final JsonElementGenerator generator = new JsonElementGenerator(element.getProject());
+ final String replacement = originalContent.substring(withoutQuotes.getStartOffset(), range.getStartOffset()) +
+ newContent +
+ originalContent.substring(range.getEndOffset(), withoutQuotes.getEndOffset());
+ return (JsonStringLiteral)element.replace(generator.createStringLiteral(replacement));
+ }
+
+ @NotNull
+ @Override
+ public TextRange getRangeInElement(@NotNull JsonStringLiteral element) {
+ final String content = element.getText();
+ final int startOffset = content.startsWith("'") || content.startsWith("\"") ? 1 : 0;
+ final int endOffset = content.length() > 1 && (content.endsWith("'") || content.endsWith("\"")) ? -1 : 0;
+ return new TextRange(startOffset, content.length() + endOffset);
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JSStringLiteralEscaper.java b/json/src/com/intellij/json/psi/impl/JSStringLiteralEscaper.java
new file mode 100644
index 00000000..1f7b7290
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JSStringLiteralEscaper.java
@@ -0,0 +1,194 @@
+package com.intellij.json.psi.impl;
+
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.LiteralTextEscaper;
+import com.intellij.psi.PsiLanguageInjectionHost;
+import org.jetbrains.annotations.NotNull;
+
+public abstract class JSStringLiteralEscaper<T extends PsiLanguageInjectionHost> extends LiteralTextEscaper<T> {
+ private int[] outSourceOffsets;
+
+ public JSStringLiteralEscaper(T host) {
+ super(host);
+ }
+
+ @Override
+ public boolean decode(@NotNull final TextRange rangeInsideHost, @NotNull StringBuilder outChars) {
+ String subText = rangeInsideHost.substring(myHost.getText());
+
+ Ref<int[]> sourceOffsetsRef = new Ref<>();
+ boolean result = parseStringCharacters(subText, outChars, sourceOffsetsRef, isRegExpLiteral(), !isOneLine());
+ outSourceOffsets = sourceOffsetsRef.get();
+ return result;
+ }
+
+ protected abstract boolean isRegExpLiteral();
+
+ @Override
+ public int getOffsetInHost(int offsetInDecoded, @NotNull final TextRange rangeInsideHost) {
+ int result = offsetInDecoded < outSourceOffsets.length ? outSourceOffsets[offsetInDecoded] : -1;
+ if (result == -1) return -1;
+ return (result <= rangeInsideHost.getLength() ? result : rangeInsideHost.getLength()) + rangeInsideHost.getStartOffset();
+ }
+
+ @Override
+ public boolean isOneLine() {
+ return true;
+ }
+
+ public static boolean parseStringCharacters(String chars, StringBuilder outChars, Ref<int[]> sourceOffsetsRef, boolean regExp, boolean escapeBacktick) {
+ int[] sourceOffsets = new int[chars.length() + 1];
+ sourceOffsetsRef.set(sourceOffsets);
+
+ if (chars.indexOf('\\') < 0) {
+ outChars.append(chars);
+ for (int i = 0; i < sourceOffsets.length; i++) {
+ sourceOffsets[i] = i;
+ }
+ return true;
+ }
+
+ int index = 0;
+ final int outOffset = outChars.length();
+ while (index < chars.length()) {
+ char c = chars.charAt(index++);
+
+ sourceOffsets[outChars.length() - outOffset] = index - 1;
+ sourceOffsets[outChars.length() + 1 - outOffset] = index;
+
+ if (c != '\\') {
+ outChars.append(c);
+ continue;
+ }
+ if (index == chars.length()) return false;
+ c = chars.charAt(index++);
+ if (escapeBacktick && c == '`') {
+ outChars.append(c);
+ }
+ else if (regExp) {
+ if (c != '/') {
+ outChars.append('\\');
+ }
+ outChars.append(c);
+ }
+ else {
+ switch (c) {
+ case 'b':
+ outChars.append('\b');
+ break;
+
+ case 't':
+ outChars.append('\t');
+ break;
+
+ case 'n':
+ outChars.append('\n');
+ break;
+
+ case 'f':
+ outChars.append('\f');
+ break;
+
+ case 'r':
+ outChars.append('\r');
+ break;
+
+ case '"':
+ outChars.append('"');
+ break;
+
+ case '/':
+ outChars.append('/');
+ break;
+
+ case '\n':
+ outChars.append('\n');
+ break;
+ case '\'':
+ outChars.append('\'');
+ break;
+
+ case '\\':
+ outChars.append('\\');
+ break;
+
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7': {
+ char startC = c;
+ int v = (int)c - '0';
+ if (index < chars.length()) {
+ c = chars.charAt(index++);
+ if ('0' <= c && c <= '7') {
+ v <<= 3;
+ v += c - '0';
+ if (startC <= '3' && index < chars.length()) {
+ c = chars.charAt(index++);
+ if ('0' <= c && c <= '7') {
+ v <<= 3;
+ v += c - '0';
+ }
+ else {
+ index--;
+ }
+ }
+ }
+ else {
+ index--;
+ }
+ }
+ outChars.append((char)v);
+ }
+ break;
+ case 'x':
+ if (index + 2 <= chars.length()) {
+ try {
+ int v = Integer.parseInt(chars.substring(index, index + 2), 16);
+ outChars.append((char)v);
+ index += 2;
+ }
+ catch (Exception e) {
+ return false;
+ }
+ }
+ else {
+ return false;
+ }
+ break;
+ case 'u':
+ if (index + 4 <= chars.length()) {
+ try {
+ int v = Integer.parseInt(chars.substring(index, index + 4), 16);
+ //line separators are invalid here
+ if (v == 0x000a || v == 0x000d) return false;
+ c = chars.charAt(index);
+ if (c == '+' || c == '-') return false;
+ outChars.append((char)v);
+ index += 4;
+ }
+ catch (Exception e) {
+ return false;
+ }
+ }
+ else {
+ return false;
+ }
+ break;
+
+ default:
+ outChars.append(c);
+ break;
+ }
+ }
+
+ sourceOffsets[outChars.length() - outOffset] = index;
+ }
+ return true;
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonElementImpl.java b/json/src/com/intellij/json/psi/impl/JsonElementImpl.java
new file mode 100644
index 00000000..e65b4b68
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonElementImpl.java
@@ -0,0 +1,23 @@
+package com.intellij.json.psi.impl;
+
+import com.intellij.extapi.psi.ASTWrapperPsiElement;
+import com.intellij.json.psi.JsonElement;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.text.StringUtil;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonElementImpl extends ASTWrapperPsiElement implements JsonElement {
+
+ public JsonElementImpl(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ @Override
+ public String toString() {
+ final String className = getClass().getSimpleName();
+ return StringUtil.trimEnd(className, "Impl");
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonFileImpl.java b/json/src/com/intellij/json/psi/impl/JsonFileImpl.java
new file mode 100644
index 00000000..8ddcdd2c
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonFileImpl.java
@@ -0,0 +1,43 @@
+package com.intellij.json.psi.impl;
+
+import com.intellij.extapi.psi.PsiFileBase;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.lang.Language;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class JsonFileImpl extends PsiFileBase implements JsonFile {
+
+ public JsonFileImpl(FileViewProvider fileViewProvider, Language language) {
+ super(fileViewProvider, language);
+ }
+
+ @NotNull
+ @Override
+ public FileType getFileType() {
+ return getViewProvider().getFileType();
+ }
+
+ @Nullable
+ @Override
+ public JsonValue getTopLevelValue() {
+ return PsiTreeUtil.getChildOfType(this, JsonValue.class);
+ }
+
+ @NotNull
+ @Override
+ public List<JsonValue> getAllTopLevelValues() {
+ return PsiTreeUtil.getChildrenOfTypeAsList(this, JsonValue.class);
+ }
+
+ @Override
+ public String toString() {
+ return "JsonFile: " + getName();
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonLiteralMixin.java b/json/src/com/intellij/json/psi/impl/JsonLiteralMixin.java
new file mode 100644
index 00000000..3ada2002
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonLiteralMixin.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+ */
+package com.intellij.json.psi.impl;
+
+import com.intellij.json.psi.JsonLiteral;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry;
+import org.jetbrains.annotations.NotNull;
+
+abstract class JsonLiteralMixin extends JsonElementImpl implements JsonLiteral {
+ protected JsonLiteralMixin(ASTNode node) {
+ super(node);
+ }
+
+ @NotNull
+ @Override
+ public PsiReference[] getReferences() {
+ return ReferenceProvidersRegistry.getReferencesFromProviders(this);
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonObjectMixin.java b/json/src/com/intellij/json/psi/impl/JsonObjectMixin.java
new file mode 100644
index 00000000..91c5c3a8
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonObjectMixin.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2000-2015 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.intellij.json.psi.impl;
+
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.util.CachedValueProvider;
+import com.intellij.psi.util.CachedValuesManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Mikhail Golubev
+ */
+public abstract class JsonObjectMixin extends JsonContainerImpl implements JsonObject {
+ private final CachedValueProvider<Map<String, JsonProperty>> myPropertyCache =
+ () -> {
+ final Map<String, JsonProperty> cache = new HashMap<>();
+ for (JsonProperty property : getPropertyList()) {
+ final String propertyName = property.getName();
+ // Preserve the old behavior - return the first value in findProperty()
+ if (!cache.containsKey(propertyName)) {
+ cache.put(propertyName, property);
+ }
+ }
+ // Cached value is invalidated every time file containing this object is modified
+ return CachedValueProvider.Result.createSingleDependency(cache, this);
+ };
+
+ public JsonObjectMixin(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ @Nullable
+ @Override
+ public JsonProperty findProperty(@NotNull String name) {
+ return CachedValuesManager.getCachedValue(this, myPropertyCache).get(name);
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonPropertyMixin.java b/json/src/com/intellij/json/psi/impl/JsonPropertyMixin.java
new file mode 100644
index 00000000..3fc29aef
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonPropertyMixin.java
@@ -0,0 +1,42 @@
+package com.intellij.json.psi.impl;
+
+import com.intellij.json.psi.JsonElementGenerator;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry;
+import com.intellij.util.ArrayUtil;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+abstract class JsonPropertyMixin extends JsonElementImpl implements JsonProperty {
+ JsonPropertyMixin(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ @Override
+ public PsiElement setName(@NonNls @NotNull String name) throws IncorrectOperationException {
+ final JsonElementGenerator generator = new JsonElementGenerator(getProject());
+ // Strip only both quotes in case user wants some exotic name like key'
+ getNameElement().replace(generator.createStringLiteral(StringUtil.unquoteString(name)));
+ return this;
+ }
+
+ @Override
+ public PsiReference getReference() {
+ return new JsonPropertyNameReference(this);
+ }
+
+ @NotNull
+ @Override
+ public PsiReference[] getReferences() {
+ final PsiReference[] fromProviders = ReferenceProvidersRegistry.getReferencesFromProviders(this);
+ return ArrayUtil.prepend(new JsonPropertyNameReference(this), fromProviders);
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonPropertyNameReference.java b/json/src/com/intellij/json/psi/impl/JsonPropertyNameReference.java
new file mode 100644
index 00000000..5b51dcb9
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonPropertyNameReference.java
@@ -0,0 +1,74 @@
+package com.intellij.json.psi.impl;
+
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.ElementManipulators;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonPropertyNameReference implements PsiReference {
+ private final JsonProperty myProperty;
+
+ public JsonPropertyNameReference(@NotNull JsonProperty property) {
+ myProperty = property;
+ }
+
+ @NotNull
+ @Override
+ public PsiElement getElement() {
+ return myProperty;
+ }
+
+ @NotNull
+ @Override
+ public TextRange getRangeInElement() {
+ final JsonValue nameElement = myProperty.getNameElement();
+ // Either value of string with quotes stripped or element's text as is
+ return ElementManipulators.getValueTextRange(nameElement);
+ }
+
+ @Nullable
+ @Override
+ public PsiElement resolve() {
+ return myProperty;
+ }
+
+ @NotNull
+ @Override
+ public String getCanonicalText() {
+ return myProperty.getName();
+ }
+
+ @Override
+ public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException {
+ return myProperty.setName(newElementName);
+ }
+
+ @Override
+ public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException {
+ return null;
+ }
+
+ @Override
+ public boolean isReferenceTo(@NotNull PsiElement element) {
+ if (!(element instanceof JsonProperty)) {
+ return false;
+ }
+ // May reference to the property with the same name for compatibility with JavaScript JSON support
+ final JsonProperty otherProperty = (JsonProperty)element;
+ final PsiElement selfResolve = resolve();
+ return otherProperty.getName().equals(getCanonicalText()) && selfResolve != otherProperty;
+ }
+
+ @Override
+ public boolean isSoft() {
+ return true;
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonPsiImplUtils.java b/json/src/com/intellij/json/psi/impl/JsonPsiImplUtils.java
new file mode 100644
index 00000000..126e3f7d
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonPsiImplUtils.java
@@ -0,0 +1,233 @@
+package com.intellij.json.psi.impl;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.json.JsonBundle;
+import com.intellij.json.JsonDialectUtil;
+import com.intellij.json.JsonLanguage;
+import com.intellij.json.JsonParserDefinition;
+import com.intellij.json.codeinsight.JsonStandardComplianceInspection;
+import com.intellij.json.psi.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.Language;
+import com.intellij.navigation.ItemPresentation;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.PlatformIcons;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class JsonPsiImplUtils {
+ static final Key<List<Pair<TextRange, String>>> STRING_FRAGMENTS = new Key<>("JSON string fragments");
+
+ @NotNull
+ public static String getName(@NotNull JsonProperty property) {
+ return StringUtil.unescapeStringCharacters(JsonPsiUtil.stripQuotes(property.getNameElement().getText()));
+ }
+
+ /**
+ * Actually only JSON string literal should be accepted as valid name of property according to standard,
+ * but for compatibility with JavaScript integration any JSON literals as well as identifiers (unquoted words)
+ * are possible and highlighted as error later.
+ *
+ * @see JsonStandardComplianceInspection
+ */
+ @NotNull
+ public static JsonValue getNameElement(@NotNull JsonProperty property) {
+ final PsiElement firstChild = property.getFirstChild();
+ assert firstChild instanceof JsonLiteral || firstChild instanceof JsonReferenceExpression;
+ return (JsonValue)firstChild;
+ }
+
+ @Nullable
+ public static JsonValue getValue(@NotNull JsonProperty property) {
+ return PsiTreeUtil.getNextSiblingOfType(getNameElement(property), JsonValue.class);
+ }
+
+ public static boolean isQuotedString(@NotNull JsonLiteral literal) {
+ return literal.getNode().findChildByType(JsonParserDefinition.STRING_LITERALS) != null;
+ }
+
+ @Nullable
+ public static ItemPresentation getPresentation(@NotNull final JsonProperty property) {
+ return new ItemPresentation() {
+ @Nullable
+ @Override
+ public String getPresentableText() {
+ return property.getName();
+ }
+
+ @Nullable
+ @Override
+ public String getLocationString() {
+ final JsonValue value = property.getValue();
+ return value instanceof JsonLiteral ? value.getText() : null;
+ }
+
+ @Nullable
+ @Override
+ public Icon getIcon(boolean unused) {
+ if (property.getValue() instanceof JsonArray) {
+ return AllIcons.Json.Property_brackets;
+ }
+ if (property.getValue() instanceof JsonObject) {
+ return AllIcons.Json.Property_braces;
+ }
+ return PlatformIcons.PROPERTY_ICON;
+ }
+ };
+ }
+
+ @Nullable
+ public static ItemPresentation getPresentation(@NotNull final JsonArray array) {
+ return new ItemPresentation() {
+ @Nullable
+ @Override
+ public String getPresentableText() {
+ return JsonBundle.message("json.array");
+ }
+
+ @Nullable
+ @Override
+ public String getLocationString() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Icon getIcon(boolean unused) {
+ return AllIcons.Json.Array;
+ }
+ };
+ }
+
+ @Nullable
+ public static ItemPresentation getPresentation(@NotNull final JsonObject object) {
+ return new ItemPresentation() {
+ @Nullable
+ @Override
+ public String getPresentableText() {
+ return JsonBundle.message("json.object");
+ }
+
+ @Nullable
+ @Override
+ public String getLocationString() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Icon getIcon(boolean unused) {
+ return AllIcons.Json.Object;
+ }
+ };
+ }
+
+ private static final String ourEscapesTable = "\"\"\\\\//b\bf\fn\nr\rt\t";
+
+ @NotNull
+ public static List<Pair<TextRange, String>> getTextFragments(@NotNull JsonStringLiteral literal) {
+ List<Pair<TextRange, String>> result = literal.getUserData(STRING_FRAGMENTS);
+ if (result == null) {
+ result = new ArrayList<>();
+ final String text = literal.getText();
+ final int length = text.length();
+ int pos = 1, unescapedSequenceStart = 1;
+ while (pos < length) {
+ if (text.charAt(pos) == '\\') {
+ if (unescapedSequenceStart != pos) {
+ result.add(Pair.create(new TextRange(unescapedSequenceStart, pos), text.substring(unescapedSequenceStart, pos)));
+ }
+ if (pos == length - 1) {
+ result.add(Pair.create(new TextRange(pos, pos + 1), "\\"));
+ break;
+ }
+ final char next = text.charAt(pos + 1);
+ switch (next) {
+ case '"':
+ case '\\':
+ case '/':
+ case 'b':
+ case 'f':
+ case 'n':
+ case 'r':
+ case 't':
+ final int idx = ourEscapesTable.indexOf(next);
+ result.add(Pair.create(new TextRange(pos, pos + 2), ourEscapesTable.substring(idx + 1, idx + 2)));
+ pos += 2;
+ break;
+ case 'u':
+ int i = pos + 2;
+ for (; i < pos + 6; i++) {
+ if (i == length || !StringUtil.isHexDigit(text.charAt(i))) {
+ break;
+ }
+ }
+ result.add(Pair.create(new TextRange(pos, i), text.substring(pos, i)));
+ pos = i;
+ break;
+ case 'x':
+ Language language = JsonDialectUtil.getLanguage(literal);
+ if (language instanceof JsonLanguage && ((JsonLanguage)language).hasPermissiveStrings()) {
+ int i2 = pos + 2;
+ for (; i2 < pos + 4; i2++) {
+ if (i2 == length || !StringUtil.isHexDigit(text.charAt(i2))) {
+ break;
+ }
+ }
+ result.add(Pair.create(new TextRange(pos, i2), text.substring(pos, i2)));
+ pos = i2;
+ break;
+ }
+ default:
+ result.add(Pair.create(new TextRange(pos, pos + 2), text.substring(pos, pos + 2)));
+ pos += 2;
+ }
+ unescapedSequenceStart = pos;
+ }
+ else {
+ pos++;
+ }
+ }
+ final int contentEnd = text.charAt(0) == text.charAt(length - 1) ? length - 1 : length;
+ if (unescapedSequenceStart < contentEnd) {
+ result.add(Pair.create(new TextRange(unescapedSequenceStart, contentEnd), text.substring(unescapedSequenceStart, contentEnd)));
+ }
+ result = Collections.unmodifiableList(result);
+ literal.putUserData(STRING_FRAGMENTS, result);
+ }
+ return result;
+ }
+
+ public static void delete(@NotNull JsonProperty property) {
+ final ASTNode myNode = property.getNode();
+ JsonPsiChangeUtils.removeCommaSeparatedFromList(myNode, myNode.getTreeParent());
+ }
+
+ @NotNull
+ public static String getValue(@NotNull JsonStringLiteral literal) {
+ return StringUtil.unescapeStringCharacters(JsonPsiUtil.stripQuotes(literal.getText()));
+ }
+
+ public static boolean isPropertyName(@NotNull JsonStringLiteral literal) {
+ final PsiElement parent = literal.getParent();
+ return parent instanceof JsonProperty && ((JsonProperty)parent).getNameElement() == literal;
+ }
+
+ public static boolean getValue(@NotNull JsonBooleanLiteral literal) {
+ return literal.textMatches("true");
+ }
+
+ public static double getValue(@NotNull JsonNumberLiteral literal) {
+ return Double.parseDouble(literal.getText());
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonRecursiveElementVisitor.java b/json/src/com/intellij/json/psi/impl/JsonRecursiveElementVisitor.java
new file mode 100644
index 00000000..3c0eb0e4
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonRecursiveElementVisitor.java
@@ -0,0 +1,17 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.psi.impl;
+
+import com.intellij.json.psi.JsonElementVisitor;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiRecursiveVisitor;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonRecursiveElementVisitor extends JsonElementVisitor implements PsiRecursiveVisitor {
+
+ @Override
+ public void visitElement(final PsiElement element) {
+ element.acceptChildren(this);
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonStringLiteralMixin.java b/json/src/com/intellij/json/psi/impl/JsonStringLiteralMixin.java
new file mode 100644
index 00000000..99baebef
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonStringLiteralMixin.java
@@ -0,0 +1,45 @@
+package com.intellij.json.psi.impl;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.LiteralTextEscaper;
+import com.intellij.psi.PsiLanguageInjectionHost;
+import com.intellij.psi.impl.source.tree.LeafElement;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Konstantin.Ulitin
+ */
+public abstract class JsonStringLiteralMixin extends JsonLiteralImpl implements PsiLanguageInjectionHost {
+ protected JsonStringLiteralMixin(ASTNode node) {
+ super(node);
+ }
+
+ @Override
+ public boolean isValidHost() {
+ return true;
+ }
+
+ @Override
+ public PsiLanguageInjectionHost updateText(@NotNull String text) {
+ ASTNode valueNode = getNode().getFirstChildNode();
+ assert valueNode instanceof LeafElement;
+ ((LeafElement)valueNode).replaceWithText(text);
+ return this;
+ }
+
+ @NotNull
+ @Override
+ public LiteralTextEscaper<? extends PsiLanguageInjectionHost> createLiteralTextEscaper() {
+ return new JSStringLiteralEscaper<PsiLanguageInjectionHost>(this) {
+ @Override
+ protected boolean isRegExpLiteral() {
+ return false;
+ }
+ };
+ }
+
+ @Override
+ public void subtreeChanged() {
+ putUserData(JsonPsiImplUtils.STRING_FRAGMENTS, null);
+ }
+}
diff --git a/json/src/com/intellij/json/psi/impl/JsonTreeChangePreprocessor.java b/json/src/com/intellij/json/psi/impl/JsonTreeChangePreprocessor.java
new file mode 100644
index 00000000..0b54775e
--- /dev/null
+++ b/json/src/com/intellij/json/psi/impl/JsonTreeChangePreprocessor.java
@@ -0,0 +1,40 @@
+/*
+ * 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.intellij.json.psi.impl;
+
+import com.intellij.json.JsonLanguage;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.impl.PsiTreeChangeEventImpl;
+import com.intellij.psi.impl.PsiTreeChangePreprocessorBase;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonTreeChangePreprocessor extends PsiTreeChangePreprocessorBase {
+ public JsonTreeChangePreprocessor(@NotNull PsiManager psiManager) {
+ super(psiManager);
+ }
+
+ @Override
+ protected boolean acceptsEvent(@NotNull PsiTreeChangeEventImpl event) {
+ return event.getFile() instanceof JsonFile;
+ }
+
+ @Override
+ protected boolean isOutOfCodeBlock(@NotNull PsiElement element) {
+ return element.getLanguage() instanceof JsonLanguage;
+ }
+} \ No newline at end of file
diff --git a/json/src/com/intellij/json/structureView/JsonStructureViewBuilderFactory.java b/json/src/com/intellij/json/structureView/JsonStructureViewBuilderFactory.java
new file mode 100644
index 00000000..9a626219
--- /dev/null
+++ b/json/src/com/intellij/json/structureView/JsonStructureViewBuilderFactory.java
@@ -0,0 +1,32 @@
+package com.intellij.json.structureView;
+
+import com.intellij.ide.structureView.StructureViewBuilder;
+import com.intellij.ide.structureView.StructureViewModel;
+import com.intellij.ide.structureView.TreeBasedStructureViewBuilder;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.lang.PsiStructureViewFactory;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonStructureViewBuilderFactory implements PsiStructureViewFactory {
+ @Nullable
+ @Override
+ public StructureViewBuilder getStructureViewBuilder(@NotNull final PsiFile psiFile) {
+ if (!(psiFile instanceof JsonFile)) {
+ return null;
+ }
+
+ return new TreeBasedStructureViewBuilder() {
+ @NotNull
+ @Override
+ public StructureViewModel createStructureViewModel(@Nullable Editor editor) {
+ return new JsonStructureViewModel(psiFile, editor);
+ }
+ };
+ }
+}
diff --git a/json/src/com/intellij/json/structureView/JsonStructureViewElement.java b/json/src/com/intellij/json/structureView/JsonStructureViewElement.java
new file mode 100644
index 00000000..df705efb
--- /dev/null
+++ b/json/src/com/intellij/json/structureView/JsonStructureViewElement.java
@@ -0,0 +1,86 @@
+package com.intellij.json.structureView;
+
+import com.intellij.ide.structureView.StructureViewTreeElement;
+import com.intellij.ide.util.treeView.smartTree.TreeElement;
+import com.intellij.json.psi.*;
+import com.intellij.navigation.ItemPresentation;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.ArrayUtil;
+import com.intellij.util.Function;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonStructureViewElement implements StructureViewTreeElement {
+ private final JsonElement myElement;
+
+ public JsonStructureViewElement(@NotNull JsonElement element) {
+ assert PsiTreeUtil.instanceOf(element, JsonFile.class, JsonProperty.class, JsonObject.class, JsonArray.class);
+ myElement = element;
+ }
+
+ @Override
+ public JsonElement getValue() {
+ return myElement;
+ }
+
+ @Override
+ public void navigate(boolean requestFocus) {
+ myElement.navigate(requestFocus);
+ }
+
+ @Override
+ public boolean canNavigate() {
+ return myElement.canNavigate();
+ }
+
+ @Override
+ public boolean canNavigateToSource() {
+ return myElement.canNavigateToSource();
+ }
+
+ @NotNull
+ @Override
+ public ItemPresentation getPresentation() {
+ final ItemPresentation presentation = myElement.getPresentation();
+ assert presentation != null;
+ return presentation;
+ }
+
+ @NotNull
+ @Override
+ public TreeElement[] getChildren() {
+ JsonElement value = null;
+ if (myElement instanceof JsonFile) {
+ value = ((JsonFile)myElement).getTopLevelValue();
+ }
+ else if (myElement instanceof JsonProperty) {
+ value = ((JsonProperty)myElement).getValue();
+ }
+ else if (PsiTreeUtil.instanceOf(myElement, JsonObject.class, JsonArray.class)) {
+ value = myElement;
+ }
+ if (value instanceof JsonObject) {
+ final JsonObject object = ((JsonObject)value);
+ return ContainerUtil.map2Array(object.getPropertyList(), TreeElement.class, (Function<JsonProperty, TreeElement>)property -> new JsonStructureViewElement(property));
+ }
+ else if (value instanceof JsonArray) {
+ final JsonArray array = (JsonArray)value;
+ final List<TreeElement> childObjects = ContainerUtil.mapNotNull(array.getValueList(), value1 -> {
+ if (value1 instanceof JsonObject && !((JsonObject)value1).getPropertyList().isEmpty()) {
+ return new JsonStructureViewElement(value1);
+ }
+ else if (value1 instanceof JsonArray && PsiTreeUtil.findChildOfType(value1, JsonProperty.class) != null) {
+ return new JsonStructureViewElement(value1);
+ }
+ return null;
+ });
+ return ArrayUtil.toObjectArray(childObjects, TreeElement.class);
+ }
+ return EMPTY_ARRAY;
+ }
+}
diff --git a/json/src/com/intellij/json/structureView/JsonStructureViewModel.java b/json/src/com/intellij/json/structureView/JsonStructureViewModel.java
new file mode 100644
index 00000000..a3bcf62b
--- /dev/null
+++ b/json/src/com/intellij/json/structureView/JsonStructureViewModel.java
@@ -0,0 +1,37 @@
+package com.intellij.json.structureView;
+
+import com.intellij.ide.structureView.StructureViewModel;
+import com.intellij.ide.structureView.StructureViewModelBase;
+import com.intellij.ide.structureView.StructureViewTreeElement;
+import com.intellij.ide.util.treeView.smartTree.Sorter;
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonStructureViewModel extends StructureViewModelBase implements StructureViewModel.ElementInfoProvider {
+
+ public JsonStructureViewModel(@NotNull PsiFile psiFile, @Nullable Editor editor) {
+ super(psiFile, editor, new JsonStructureViewElement((JsonFile)psiFile));
+ withSuitableClasses(JsonFile.class, JsonProperty.class, JsonObject.class, JsonArray.class);
+ withSorters(Sorter.ALPHA_SORTER);
+ }
+
+ @Override
+ public boolean isAlwaysShowsPlus(StructureViewTreeElement element) {
+ return false;
+ }
+
+ @Override
+ public boolean isAlwaysLeaf(StructureViewTreeElement element) {
+ return false;
+ }
+
+}
diff --git a/json/src/com/intellij/json/surroundWith/JsonSurroundDescriptor.java b/json/src/com/intellij/json/surroundWith/JsonSurroundDescriptor.java
new file mode 100644
index 00000000..60f32e84
--- /dev/null
+++ b/json/src/com/intellij/json/surroundWith/JsonSurroundDescriptor.java
@@ -0,0 +1,84 @@
+package com.intellij.json.surroundWith;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.lang.surroundWith.SurroundDescriptor;
+import com.intellij.lang.surroundWith.Surrounder;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiWhiteSpace;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class JsonSurroundDescriptor implements SurroundDescriptor {
+ private static final Surrounder[] ourSurrounders = new Surrounder[]{
+ new JsonWithObjectLiteralSurrounder(),
+ new JsonWithArrayLiteralSurrounder(),
+ new JsonWithQuotesSurrounder()
+ };
+
+ @NotNull
+ @Override
+ public PsiElement[] getElementsToSurround(PsiFile file, int startOffset, int endOffset) {
+ PsiElement firstElement = file.findElementAt(startOffset);
+ PsiElement lastElement = file.findElementAt(endOffset - 1);
+
+ // Extend selection beyond possible delimiters
+ while (firstElement != null &&
+ (firstElement instanceof PsiWhiteSpace || firstElement.getNode().getElementType() == JsonElementTypes.COMMA)) {
+ firstElement = firstElement.getNextSibling();
+ }
+ while (lastElement != null &&
+ (lastElement instanceof PsiWhiteSpace || lastElement.getNode().getElementType() == JsonElementTypes.COMMA)) {
+ lastElement = lastElement.getPrevSibling();
+ }
+ if (firstElement != null) {
+ startOffset = firstElement.getTextRange().getStartOffset();
+ }
+ if (lastElement != null) {
+ endOffset = lastElement.getTextRange().getEndOffset();
+ }
+
+ final JsonProperty property = PsiTreeUtil.findElementOfClassAtRange(file, startOffset, endOffset, JsonProperty.class);
+ if (property != null) {
+ return collectElements(endOffset, property, JsonProperty.class);
+ }
+
+ final JsonValue value = PsiTreeUtil.findElementOfClassAtRange(file, startOffset, endOffset, JsonValue.class);
+ if (value != null) {
+ return collectElements(endOffset, value, JsonValue.class);
+ }
+ return PsiElement.EMPTY_ARRAY;
+ }
+
+ @NotNull
+ private static <T extends PsiElement> PsiElement[] collectElements(int endOffset, @NotNull T property, @NotNull Class<T> kind) {
+ final List<T> properties = ContainerUtil.newArrayList(property);
+ PsiElement nextSibling = property.getNextSibling();
+ while (nextSibling != null && nextSibling.getTextRange().getEndOffset() <= endOffset) {
+ if (kind.isInstance(nextSibling)) {
+ properties.add(kind.cast(nextSibling));
+ }
+ nextSibling = nextSibling.getNextSibling();
+ }
+ return properties.toArray(PsiElement.EMPTY_ARRAY);
+ }
+
+ @NotNull
+ @Override
+ public Surrounder[] getSurrounders() {
+ return ourSurrounders;
+ }
+
+ @Override
+ public boolean isExclusive() {
+ return false;
+ }
+}
diff --git a/json/src/com/intellij/json/surroundWith/JsonSurrounderBase.java b/json/src/com/intellij/json/surroundWith/JsonSurrounderBase.java
new file mode 100644
index 00000000..f4321e71
--- /dev/null
+++ b/json/src/com/intellij/json/surroundWith/JsonSurrounderBase.java
@@ -0,0 +1,57 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.surroundWith;
+
+import com.intellij.json.psi.JsonElementGenerator;
+import com.intellij.json.psi.JsonPsiUtil;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.lang.surroundWith.Surrounder;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public abstract class JsonSurrounderBase implements Surrounder {
+ @Override
+ public boolean isApplicable(@NotNull PsiElement[] elements) {
+ return elements.length >= 1 && elements[0] instanceof JsonValue && !JsonPsiUtil.isPropertyKey(elements[0]);
+ }
+
+ @Nullable
+ @Override
+ public TextRange surroundElements(@NotNull Project project, @NotNull Editor editor, @NotNull PsiElement[] elements)
+ throws IncorrectOperationException {
+ if (!isApplicable(elements)) {
+ return null;
+ }
+
+ final JsonElementGenerator generator = new JsonElementGenerator(project);
+
+ if (elements.length == 1) {
+ JsonValue replacement = generator.createValue(createReplacementText(elements[0].getText()));
+ elements[0].replace(replacement);
+ }
+ else {
+ final String propertiesText = getTextAndRemoveMisc(elements[0], elements[elements.length - 1]);
+ JsonValue replacement = generator.createValue(createReplacementText(propertiesText));
+ elements[0].replace(replacement);
+ }
+ return null;
+ }
+
+ @NotNull
+ protected static String getTextAndRemoveMisc(@NotNull PsiElement firstProperty, @NotNull PsiElement lastProperty) {
+ final TextRange replacedRange = new TextRange(firstProperty.getTextOffset(), lastProperty.getTextRange().getEndOffset());
+ final String propertiesText = replacedRange.substring(firstProperty.getContainingFile().getText());
+ if (firstProperty != lastProperty) {
+ final PsiElement parent = firstProperty.getParent();
+ parent.deleteChildRange(firstProperty.getNextSibling(), lastProperty);
+ }
+ return propertiesText;
+ }
+
+ @NotNull
+ protected abstract String createReplacementText(@NotNull String textInRange);
+}
diff --git a/json/src/com/intellij/json/surroundWith/JsonWithArrayLiteralSurrounder.java b/json/src/com/intellij/json/surroundWith/JsonWithArrayLiteralSurrounder.java
new file mode 100644
index 00000000..7f6091f3
--- /dev/null
+++ b/json/src/com/intellij/json/surroundWith/JsonWithArrayLiteralSurrounder.java
@@ -0,0 +1,18 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.surroundWith;
+
+import com.intellij.json.JsonBundle;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonWithArrayLiteralSurrounder extends JsonSurrounderBase {
+ @Override
+ public String getTemplateDescription() {
+ return JsonBundle.message("surround.with.array.literal.desc");
+ }
+
+ @NotNull
+ @Override
+ protected String createReplacementText(@NotNull String firstElement) {
+ return "[" + firstElement + "]";
+ }
+}
diff --git a/json/src/com/intellij/json/surroundWith/JsonWithObjectLiteralSurrounder.java b/json/src/com/intellij/json/surroundWith/JsonWithObjectLiteralSurrounder.java
new file mode 100644
index 00000000..40182965
--- /dev/null
+++ b/json/src/com/intellij/json/surroundWith/JsonWithObjectLiteralSurrounder.java
@@ -0,0 +1,85 @@
+package com.intellij.json.surroundWith;
+
+import com.intellij.json.JsonBundle;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * This surrounder ported from JavaScript allows to wrap single JSON value or several consecutive JSON properties
+ * in object literal.
+ * <p/>
+ * Examples:
+ * <ol>
+ * <li>{@code [42]} converts to {@code [{"property": 42}]}</li>
+ * <li><pre>
+ * {
+ * "foo": 42,
+ * "bar": false
+ * }
+ * </pre> converts to <pre>
+ * {
+ * "property": {
+ * "foo": 42,
+ * "bar": false
+ * }
+ * }
+ * </pre></li>
+ * </ol>
+ *
+ * @author Mikhail Golubev
+ */
+public class JsonWithObjectLiteralSurrounder extends JsonSurrounderBase {
+ @Override
+ public String getTemplateDescription() {
+ return JsonBundle.message("surround.with.object.literal.desc");
+ }
+
+ @Override
+ public boolean isApplicable(@NotNull PsiElement[] elements) {
+ return !JsonPsiUtil.isPropertyKey(elements[0]) && (elements[0] instanceof JsonProperty || elements.length == 1);
+ }
+
+ @Nullable
+ @Override
+ public TextRange surroundElements(@NotNull Project project,
+ @NotNull Editor editor,
+ @NotNull PsiElement[] elements) throws IncorrectOperationException {
+
+ if (!isApplicable(elements)) {
+ return null;
+ }
+
+ final JsonElementGenerator generator = new JsonElementGenerator(project);
+
+ final PsiElement firstElement = elements[0];
+ final JsonElement newNameElement;
+ if (firstElement instanceof JsonValue) {
+ assert elements.length == 1 : "Only single JSON value can be wrapped in object literal";
+ JsonObject replacement = generator.createValue(createReplacementText(firstElement.getText()));
+ replacement = (JsonObject)firstElement.replace(replacement);
+ newNameElement = replacement.getPropertyList().get(0).getNameElement();
+ }
+ else {
+ assert firstElement instanceof JsonProperty;
+ final String propertiesText = getTextAndRemoveMisc(firstElement, elements[elements.length - 1]);
+ final JsonObject tempJsonObject = generator.createValue(createReplacementText("{\n" + propertiesText) + "\n}");
+ JsonProperty replacement = tempJsonObject.getPropertyList().get(0);
+ replacement = (JsonProperty)firstElement.replace(replacement);
+ newNameElement = replacement.getNameElement();
+ }
+ final TextRange rangeWithQuotes = newNameElement.getTextRange();
+ return new TextRange(rangeWithQuotes.getStartOffset() + 1, rangeWithQuotes.getEndOffset() - 1);
+ }
+
+ @NotNull
+ @Override
+ protected String createReplacementText(@NotNull String textInRange) {
+ return "{\n\"property\": " + textInRange + "\n}";
+ }
+}
diff --git a/json/src/com/intellij/json/surroundWith/JsonWithQuotesSurrounder.java b/json/src/com/intellij/json/surroundWith/JsonWithQuotesSurrounder.java
new file mode 100644
index 00000000..665090f6
--- /dev/null
+++ b/json/src/com/intellij/json/surroundWith/JsonWithQuotesSurrounder.java
@@ -0,0 +1,19 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.json.surroundWith;
+
+import com.intellij.json.JsonBundle;
+import com.intellij.openapi.util.text.StringUtil;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonWithQuotesSurrounder extends JsonSurrounderBase {
+ @Override
+ public String getTemplateDescription() {
+ return JsonBundle.message("surround.with.quotes.desc");
+ }
+
+ @NotNull
+ @Override
+ protected String createReplacementText(@NotNull String firstElement) {
+ return "\"" + StringUtil.escapeStringCharacters(firstElement) + "\"";
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonMappingKind.java b/json/src/com/jetbrains/jsonSchema/JsonMappingKind.java
new file mode 100644
index 00000000..abcb943e
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonMappingKind.java
@@ -0,0 +1,41 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.util.text.StringUtil;
+
+import javax.swing.*;
+
+public enum JsonMappingKind {
+ File,
+ Pattern,
+ Directory;
+
+ public String getDescription() {
+ switch (this) {
+ case File:
+ return "file";
+ case Pattern:
+ return "file path pattern";
+ case Directory:
+ return "directory";
+ }
+ return "";
+ }
+
+ public String getPrefix() {
+ return StringUtil.capitalize(getDescription()) + ": ";
+ }
+
+ public Icon getIcon() {
+ switch (this) {
+ case File:
+ return AllIcons.FileTypes.Any_type;
+ case Pattern:
+ return AllIcons.FileTypes.Unknown;
+ case Directory:
+ return AllIcons.Nodes.Folder;
+ }
+ return null;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonPointerResolver.java b/json/src/com/jetbrains/jsonSchema/JsonPointerResolver.java
new file mode 100644
index 00000000..e2c87367
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonPointerResolver.java
@@ -0,0 +1,60 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonValue;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVariantsTreeBuilder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class JsonPointerResolver {
+ private final JsonValue myRoot;
+ private final String myPointer;
+
+ public JsonPointerResolver(@NotNull JsonValue root, @NotNull String pointer) {
+ myRoot = root;
+ myPointer = pointer;
+ }
+
+ @Nullable
+ public JsonValue resolve() {
+ JsonValue root = myRoot;
+ final List<JsonSchemaVariantsTreeBuilder.Step> steps = JsonSchemaVariantsTreeBuilder.buildSteps(myPointer);
+ for (JsonSchemaVariantsTreeBuilder.Step step : steps) {
+ String name = step.getName();
+ if (name != null) {
+ if (!(root instanceof JsonObject)) return null;
+ JsonProperty property = ((JsonObject)root).findProperty(name);
+ root = property == null ? null : property.getValue();
+ }
+ else {
+ int idx = step.getIdx();
+ if (idx < 0) return null;
+
+ if (!(root instanceof JsonArray)) {
+ if (root instanceof JsonObject) {
+ JsonProperty property = ((JsonObject)root).findProperty(String.valueOf(idx));
+ if (property == null) {
+ return null;
+ }
+ root = property.getValue();
+ continue;
+ }
+ else {
+ return null;
+ }
+ }
+ List<JsonValue> list = ((JsonArray)root).getValueList();
+ if (idx >= list.size()) return null;
+ root = list.get(idx);
+ }
+ }
+ return root;
+ }
+
+
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonPointerUtil.java b/json/src/com/jetbrains/jsonSchema/JsonPointerUtil.java
new file mode 100644
index 00000000..2cb1c37c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonPointerUtil.java
@@ -0,0 +1,38 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.util.io.URLUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class JsonPointerUtil {
+ @NotNull
+ public static String escapeForJsonPointer(@NotNull String name) {
+ if (StringUtil.isEmptyOrSpaces(name)) {
+ return URLUtil.encodeURIComponent(name);
+ }
+ return StringUtil.replace(StringUtil.replace(name, "~", "~0"), "/", "~1");
+ }
+
+ @NotNull
+ public static String unescapeJsonPointerPart(@NotNull String part) {
+ part = URLUtil.unescapePercentSequences(part);
+ return StringUtil.replace(StringUtil.replace(part, "~0", "~"), "~1", "/");
+ }
+
+ public static boolean isSelfReference(@Nullable String ref) {
+ return "#".equals(ref) || "#/".equals(ref) || StringUtil.isEmpty(ref);
+ }
+
+ public static List<String> split(@NotNull String pointer) {
+ return StringUtil.split(pointer, "/", true, false);
+ }
+
+ @NotNull
+ public static String normalizeSlashes(@NotNull String ref) {
+ return StringUtil.trimStart(ref.replace('\\', '/'), "/");
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogConfigurable.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogConfigurable.java
new file mode 100644
index 00000000..96b838fe
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogConfigurable.java
@@ -0,0 +1,110 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.openapi.options.Configurable;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.panel.ComponentPanelBuilder;
+import com.intellij.ui.components.JBCheckBox;
+import com.intellij.ui.components.JBPanel;
+import com.intellij.util.ui.FormBuilder;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class JsonSchemaCatalogConfigurable implements Configurable {
+ @NonNls public static final String SETTINGS_JSON_SCHEMA_CATALOG = "settings.json.schema.catalog";
+ public static final String JSON_SCHEMA_CATALOG = "Remote JSON Schemas";
+ @NotNull private final Project myProject;
+ private final JBCheckBox myCatalogCheckBox;
+ private final JBCheckBox myRemoteCheckBox;
+ private final JBCheckBox myPreferRemoteCheckBox;
+
+ public JsonSchemaCatalogConfigurable(@NotNull final Project project) {
+ myProject = project;
+ myCatalogCheckBox = new JBCheckBox("Use schemastore.org JSON Schema catalog");
+ myRemoteCheckBox = new JBCheckBox("Allow downloading JSON Schemas from remote sources");
+ myPreferRemoteCheckBox = new JBCheckBox("Always download the most recent version of schemas");
+ }
+
+ @Nullable
+ @Override
+ public JComponent createComponent() {
+ FormBuilder builder = FormBuilder.createFormBuilder();
+
+ builder.addComponent(myRemoteCheckBox);
+ builder.addVerticalGap(2);
+ myRemoteCheckBox.addChangeListener(c -> {
+ boolean selected = myRemoteCheckBox.isSelected();
+ myCatalogCheckBox.setEnabled(selected);
+ myPreferRemoteCheckBox.setEnabled(selected);
+ if (!selected) {
+ myCatalogCheckBox.setSelected(false);
+ myPreferRemoteCheckBox.setSelected(false);
+ }
+ });
+ addWithComment(builder, myCatalogCheckBox,
+ "Schemas will be downloaded and assigned using the <a href=\"http://schemastore.org/json/\">SchemaStore API</a>");
+ addWithComment(builder, myPreferRemoteCheckBox,
+ "Schemas will always be downloaded from the SchemaStore, even if some of them are bundled with the IDE");
+ return wrap(builder.getPanel());
+ }
+
+ private static void addWithComment(FormBuilder builder, JBCheckBox box, String s) {
+ builder.addComponent(new ComponentPanelBuilder(box).withComment(s).createPanel());
+ }
+
+ private static JPanel wrap(JComponent panel) {
+ JPanel wrapper = new JBPanel(new BorderLayout());
+ wrapper.add(panel, BorderLayout.NORTH);
+ return wrapper;
+ }
+
+ @Override
+ public JComponent getPreferredFocusedComponent() {
+ return myCatalogCheckBox;
+ }
+
+ @Override
+ public void reset() {
+ JsonSchemaCatalogProjectConfiguration.MyState state = JsonSchemaCatalogProjectConfiguration.getInstance(myProject).getState();
+ final boolean remoteEnabled = state == null || state.myIsRemoteActivityEnabled;
+ myRemoteCheckBox.setSelected(remoteEnabled);
+ myCatalogCheckBox.setEnabled(remoteEnabled);
+ myPreferRemoteCheckBox.setEnabled(remoteEnabled);
+ myCatalogCheckBox.setSelected(state == null || state.myIsCatalogEnabled);
+ myPreferRemoteCheckBox.setSelected(state == null || state.myIsPreferRemoteSchemas);
+ }
+
+ @Override
+ public boolean isModified() {
+ JsonSchemaCatalogProjectConfiguration.MyState state = JsonSchemaCatalogProjectConfiguration.getInstance(myProject).getState();
+ return state == null
+ || state.myIsCatalogEnabled != myCatalogCheckBox.isSelected()
+ || state.myIsPreferRemoteSchemas != myPreferRemoteCheckBox.isSelected()
+ || state.myIsRemoteActivityEnabled != myRemoteCheckBox.isSelected();
+ }
+
+ @Override
+ public void apply() throws ConfigurationException {
+ JsonSchemaCatalogProjectConfiguration.getInstance(myProject).setState(myCatalogCheckBox.isSelected(),
+ myRemoteCheckBox.isSelected(),
+ myPreferRemoteCheckBox.isSelected());
+ }
+
+ @Nls(capitalization = Nls.Capitalization.Title)
+ @Override
+ public String getDisplayName() {
+ return JSON_SCHEMA_CATALOG;
+ }
+
+ @Nullable
+ @Override
+ public String getHelpTopic() {
+ return SETTINGS_JSON_SCHEMA_CATALOG;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogProjectConfiguration.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogProjectConfiguration.java
new file mode 100644
index 00000000..2a2c3abf
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogProjectConfiguration.java
@@ -0,0 +1,86 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.openapi.components.PersistentStateComponent;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.components.State;
+import com.intellij.openapi.components.Storage;
+import com.intellij.openapi.project.Project;
+import com.intellij.util.containers.ConcurrentList;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.xmlb.annotations.Tag;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+@State(name = "JsonSchemaCatalogProjectConfiguration", storages = @Storage("jsonCatalog.xml"))
+public class JsonSchemaCatalogProjectConfiguration implements PersistentStateComponent<JsonSchemaCatalogProjectConfiguration.MyState> {
+ public volatile MyState myState = new MyState();
+ private final ConcurrentList<Runnable> myChangeHandlers = ContainerUtil.createConcurrentList();
+
+ public boolean isCatalogEnabled() {
+ MyState state = getState();
+ return state != null && state.myIsCatalogEnabled;
+ }
+
+ public boolean isPreferRemoteSchemas() {
+ MyState state = getState();
+ return state != null && state.myIsPreferRemoteSchemas;
+ }
+
+ public void addChangeHandler(Runnable runnable) {
+ myChangeHandlers.add(runnable);
+ }
+
+ public static JsonSchemaCatalogProjectConfiguration getInstance(@NotNull final Project project) {
+ return ServiceManager.getService(project, JsonSchemaCatalogProjectConfiguration.class);
+ }
+
+ public JsonSchemaCatalogProjectConfiguration() {
+ }
+
+ public void setState(boolean isEnabled, boolean isRemoteActivityEnabled, boolean isPreferRemoteSchemas) {
+ myState = new MyState(isEnabled, isRemoteActivityEnabled, isPreferRemoteSchemas);
+ for (Runnable handler : myChangeHandlers) {
+ handler.run();
+ }
+ }
+
+ @Nullable
+ @Override
+ public MyState getState() {
+ return myState;
+ }
+
+ public boolean isRemoteActivityEnabled() {
+ MyState state = getState();
+ return state != null && state.myIsRemoteActivityEnabled;
+ }
+
+ @Override
+ public void loadState(@NotNull MyState state) {
+ myState = state;
+ for (Runnable handler : myChangeHandlers) {
+ handler.run();
+ }
+ }
+
+ static class MyState {
+ @Tag("enabled")
+ public boolean myIsCatalogEnabled = true;
+
+ @Tag("remoteActivityEnabled")
+ public boolean myIsRemoteActivityEnabled = true;
+
+ @Tag("preferRemoteSchemas")
+ public boolean myIsPreferRemoteSchemas = false;
+
+ MyState() {
+ }
+
+ MyState(boolean isCatalogEnabled, boolean isRemoteActivityEnabled, boolean isPreferRemoteSchemas) {
+ myIsCatalogEnabled = isCatalogEnabled;
+ myIsRemoteActivityEnabled = isRemoteActivityEnabled;
+ myIsPreferRemoteSchemas = isPreferRemoteSchemas;
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaFileType.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaFileType.java
new file mode 100644
index 00000000..48d49167
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaFileType.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2000-2017 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.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+/**
+ * To make plugin github.com/BlueBoxWare/LibGDXPlugin happy
+ * @author Irina.Chernushina on 4/1/2016.
+ */
+public class JsonSchemaFileType extends LanguageFileType implements FileTypeIdentifiableByVirtualFile {
+ public static final 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";
+ }
+
+ @NotNull
+ @Override
+ public String getDefaultExtension() {
+ return "json";
+ }
+
+ @Nullable
+ @Override
+ public Icon getIcon() {
+ return AllIcons.FileTypes.JsonSchema;
+ }
+
+ @Override
+ public boolean isMyFileType(@NotNull VirtualFile file) {
+ return false;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaIconProvider.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaIconProvider.java
new file mode 100644
index 00000000..78014791
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaIconProvider.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2000-2017 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.ide.IconProvider;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+/**
+ * @author Irina.Chernushina on 5/23/2017.
+ */
+public class JsonSchemaIconProvider extends IconProvider {
+ @Nullable
+ @Override
+ public Icon getIcon(@NotNull PsiElement element, int flags) {
+ if (element instanceof PsiFile && JsonSchemaService.isSchemaFile((PsiFile)element)) {
+ return AllIcons.FileTypes.JsonSchema;
+ }
+ return null;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaMappingsProjectConfiguration.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaMappingsProjectConfiguration.java
new file mode 100644
index 00000000..ace33637
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaMappingsProjectConfiguration.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+ */
+package com.jetbrains.jsonSchema;
+
+import com.intellij.openapi.components.PersistentStateComponent;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.components.State;
+import com.intellij.openapi.components.Storage;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.xmlb.annotations.Tag;
+import com.intellij.util.xmlb.annotations.XCollection;
+import com.jetbrains.jsonSchema.extension.JsonSchemaInfo;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.*;
+
+@State(name = "JsonSchemaMappingsProjectConfiguration", storages = @Storage("jsonSchemas.xml"))
+public class JsonSchemaMappingsProjectConfiguration implements PersistentStateComponent<JsonSchemaMappingsProjectConfiguration.MyState> {
+ @NotNull private final Project myProject;
+ public volatile MyState myState = new MyState();
+
+ @Nullable
+ public UserDefinedJsonSchemaConfiguration findMappingBySchemaInfo(JsonSchemaInfo value) {
+ for (UserDefinedJsonSchemaConfiguration configuration : myState.myState.values()) {
+ if (areSimilar(value, configuration)) return configuration;
+ }
+ return null;
+ }
+
+ public boolean areSimilar(JsonSchemaInfo value, UserDefinedJsonSchemaConfiguration configuration) {
+ return Objects.equals(normalizePath(value.getUrl(myProject)), normalizePath(configuration.getRelativePathToSchema()));
+ }
+
+ @Nullable
+ @Contract("null -> null; !null -> !null")
+ public String normalizePath(@Nullable String valueUrl) {
+ if (valueUrl == null) return null;
+ if (StringUtil.contains(valueUrl, "..")) {
+ valueUrl = new File(valueUrl).getAbsolutePath();
+ }
+ return valueUrl.replace('\\', '/');
+ }
+
+ @Nullable
+ public UserDefinedJsonSchemaConfiguration findMappingForFile(VirtualFile file) {
+ VirtualFile projectBaseDir = myProject.getBaseDir();
+ for (UserDefinedJsonSchemaConfiguration configuration : myState.myState.values()) {
+ for (UserDefinedJsonSchemaConfiguration.Item pattern : configuration.patterns) {
+ if (pattern.mappingKind != JsonMappingKind.File) continue;
+ VirtualFile relativeFile = VfsUtil.findRelativeFile(projectBaseDir, pattern.getPathParts());
+ if (Objects.equals(relativeFile, file) || file.getUrl().equals(pattern.getPath())) {
+ return configuration;
+ }
+ }
+ }
+ return null;
+ }
+
+ public static JsonSchemaMappingsProjectConfiguration getInstance(@NotNull final Project project) {
+ return ServiceManager.getService(project, JsonSchemaMappingsProjectConfiguration.class);
+ }
+
+ public JsonSchemaMappingsProjectConfiguration(@NotNull Project project) {
+ myProject = project;
+ }
+
+ @Nullable
+ @Override
+ public MyState getState() {
+ return myState;
+ }
+
+ public void schemaFileMoved(@NotNull final Project project,
+ @NotNull final String oldRelativePath,
+ @NotNull final String newRelativePath) {
+ final Optional<UserDefinedJsonSchemaConfiguration> old = myState.myState.values().stream()
+ .filter(schema -> FileUtil.pathsEqual(schema.getRelativePathToSchema(), oldRelativePath))
+ .findFirst();
+ old.ifPresent(configuration -> {
+ configuration.setRelativePathToSchema(newRelativePath);
+ JsonSchemaService.Impl.get(project).reset();
+ });
+ }
+
+ public void removeConfiguration(UserDefinedJsonSchemaConfiguration configuration) {
+ for (Map.Entry<String, UserDefinedJsonSchemaConfiguration> entry : myState.myState.entrySet()) {
+ if (entry.getValue() == configuration) {
+ myState.myState.remove(entry.getKey());
+ return;
+ }
+ }
+ }
+
+ public void addConfiguration(UserDefinedJsonSchemaConfiguration configuration) {
+ String name = configuration.getName();
+ while (myState.myState.containsKey(name)) {
+ name += "1";
+ }
+ myState.myState.put(name, configuration);
+ }
+
+ public Map<String, UserDefinedJsonSchemaConfiguration> getStateMap() {
+ return Collections.unmodifiableMap(myState.myState);
+ }
+
+ @Override
+ public void loadState(@NotNull MyState state) {
+ myState = state;
+ JsonSchemaService.Impl.get(myProject).reset();
+ }
+
+ public void setState(@NotNull Map<String, UserDefinedJsonSchemaConfiguration> state) {
+ myState = new MyState(state);
+ }
+
+ static class MyState {
+ @Tag("state")
+ @XCollection
+ public Map<String, UserDefinedJsonSchemaConfiguration> myState = new TreeMap<>();
+
+ MyState() {
+ }
+
+ MyState(Map<String, UserDefinedJsonSchemaConfiguration> state) {
+ myState = state;
+ }
+ }
+} \ No newline at end of file
diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaRefactoringListenerProvider.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaRefactoringListenerProvider.java
new file mode 100644
index 00000000..5e4ecda6
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaRefactoringListenerProvider.java
@@ -0,0 +1,67 @@
+/*
+ * 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.json.JsonLanguage;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiUtilBase;
+import com.intellij.refactoring.listeners.RefactoringElementListener;
+import com.intellij.refactoring.listeners.RefactoringElementListenerProvider;
+import com.intellij.refactoring.listeners.UndoRefactoringElementAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Irina.Chernushina on 2/17/2016.
+ */
+public class JsonSchemaRefactoringListenerProvider implements RefactoringElementListenerProvider {
+ @Nullable
+ @Override
+ public RefactoringElementListener getListener(PsiElement element) {
+ if (element == null) {
+ return null;
+ }
+ final VirtualFile oldFile = PsiUtilBase.asVirtualFile(element);
+ if (oldFile == null || !(oldFile.getFileType() instanceof LanguageFileType) ||
+ !(((LanguageFileType)oldFile.getFileType()).getLanguage().isKindOf(JsonLanguage.INSTANCE))) {
+ return null;
+ }
+ final Project project = element.getProject();
+ if (project.getBaseDir() == null) return null;
+
+ final String oldRelativePath = VfsUtilCore.getRelativePath(oldFile, project.getBaseDir());
+ if (oldRelativePath != null) {
+ final JsonSchemaMappingsProjectConfiguration configuration = JsonSchemaMappingsProjectConfiguration.getInstance(project);
+ return new UndoRefactoringElementAdapter() {
+ @Override
+ protected void refactored(@NotNull PsiElement element, @Nullable String oldQualifiedName) {
+ final VirtualFile newFile = PsiUtilBase.asVirtualFile(element);
+ if (newFile != null) {
+ final String newRelativePath = VfsUtilCore.getRelativePath(newFile, project.getBaseDir());
+ if (newRelativePath != null) {
+ configuration.schemaFileMoved(project, oldRelativePath, newRelativePath);
+ }
+ }
+ }
+ };
+ }
+ return null;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaVfsListener.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaVfsListener.java
new file mode 100644
index 00000000..b778efae
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaVfsListener.java
@@ -0,0 +1,115 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema;
+
+import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
+import com.intellij.json.JsonFileType;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.EditorFactory;
+import com.intellij.openapi.editor.ex.EditorEx;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.ZipperUpdater;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileContentsChangedAdapter;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.vfs.impl.BulkVirtualFileListenerAdapter;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.PsiTreeAnyChangeAbstractAdapter;
+import com.intellij.util.Alarm;
+import com.intellij.util.concurrency.SequentialTaskExecutor;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.messages.MessageBusConnection;
+import com.intellij.util.messages.Topic;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonSchemaServiceImpl;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * @author Irina.Chernushina on 3/30/2016.
+ */
+public class JsonSchemaVfsListener extends BulkVirtualFileListenerAdapter {
+ public static final Topic<Runnable> JSON_SCHEMA_CHANGED = Topic.create("JsonSchemaVfsListener.Json.Schema.Changed", Runnable.class);
+ public static final Topic<Runnable> JSON_DEPS_CHANGED = Topic.create("JsonSchemaVfsListener.Json.Deps.Changed", Runnable.class);
+
+ public static void startListening(@NotNull Project project, @NotNull JsonSchemaService service, @NotNull MessageBusConnection connection) {
+ final MyUpdater updater = new MyUpdater(project, service);
+ connection.subscribe(VirtualFileManager.VFS_CHANGES, new JsonSchemaVfsListener(updater));
+ PsiManager.getInstance(project).addPsiTreeChangeListener(new PsiTreeAnyChangeAbstractAdapter() {
+ @Override
+ protected void onChange(@Nullable PsiFile file) {
+ if (file != null) updater.onFileChange(file.getViewProvider().getVirtualFile());
+ }
+ });
+ }
+
+ private JsonSchemaVfsListener(@NotNull MyUpdater updater) {
+ super(new VirtualFileContentsChangedAdapter() {
+ @NotNull private final MyUpdater myUpdater = updater;
+ @Override
+ protected void onFileChange(@NotNull final VirtualFile schemaFile) {
+ myUpdater.onFileChange(schemaFile);
+ }
+
+ @Override
+ protected void onBeforeFileChange(@NotNull VirtualFile schemaFile) {
+ myUpdater.onFileChange(schemaFile);
+ }
+ });
+ }
+
+ private static class MyUpdater {
+ @NotNull private final Project myProject;
+ private final ZipperUpdater myUpdater;
+ @NotNull private final JsonSchemaService myService;
+ private final Set<VirtualFile> myDirtySchemas = ContainerUtil.newConcurrentSet();
+ private final Runnable myRunnable;
+ private final ExecutorService myTaskExecutor = SequentialTaskExecutor.createSequentialApplicationPoolExecutor(
+ "JsonVfsUpdaterExecutor");
+
+ protected MyUpdater(@NotNull Project project, @NotNull JsonSchemaService service) {
+ myProject = project;
+ myUpdater = new ZipperUpdater(200, Alarm.ThreadToUse.POOLED_THREAD, project);
+ myService = service;
+ myRunnable = () -> {
+ if (myProject.isDisposed()) return;
+ Collection<VirtualFile> scope = new HashSet<>(myDirtySchemas);
+ if (scope.stream().anyMatch(f -> service.possiblyHasReference(f.getName()))) {
+ myProject.getMessageBus().syncPublisher(JSON_DEPS_CHANGED).run();
+ }
+ myDirtySchemas.removeAll(scope);
+ if (scope.isEmpty()) return;
+
+ Collection<VirtualFile> finalScope = ContainerUtil.filter(scope, file -> myService.isApplicableToFile(file)
+ && ((JsonSchemaServiceImpl)myService).isMappedSchema(file, false));
+ if (finalScope.isEmpty()) return;
+ if (myProject.isDisposed()) return;
+ myProject.getMessageBus().syncPublisher(JSON_SCHEMA_CHANGED).run();
+
+ final DaemonCodeAnalyzer analyzer = DaemonCodeAnalyzer.getInstance(project);
+ final PsiManager psiManager = PsiManager.getInstance(project);
+ final Editor[] editors = EditorFactory.getInstance().getAllEditors();
+ Arrays.stream(editors).filter(editor -> editor instanceof EditorEx)
+ .map(editor -> ((EditorEx)editor).getVirtualFile())
+ .filter(file -> file != null && file.isValid())
+ .forEach(file -> {
+ final Collection<VirtualFile> schemaFiles = ((JsonSchemaServiceImpl)myService).getSchemasForFile(file, false, true);
+ if (schemaFiles.stream().anyMatch(finalScope::contains)) {
+ ReadAction.nonBlocking(() -> Optional.ofNullable(psiManager.findFile(file)).ifPresent(analyzer::restart)).submit(myTaskExecutor);
+ }
+ });
+ };
+ }
+
+ protected void onFileChange(@NotNull final VirtualFile schemaFile) {
+ if (JsonFileType.DEFAULT_EXTENSION.equals(schemaFile.getExtension())) {
+ myDirtySchemas.add(schemaFile);
+ myUpdater.queue(myRunnable);
+ }
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/UserDefinedJsonSchemaConfiguration.java b/json/src/com/jetbrains/jsonSchema/UserDefinedJsonSchemaConfiguration.java
new file mode 100644
index 00000000..8ab70270
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/UserDefinedJsonSchemaConfiguration.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2000-2017 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.project.Project;
+import com.intellij.openapi.util.AtomicClearableLazyValue;
+import com.intellij.openapi.util.io.FileUtilRt;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.ArrayUtil;
+import com.intellij.util.PairProcessor;
+import com.intellij.util.PatternUtil;
+import com.intellij.util.SmartList;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.xmlb.annotations.Tag;
+import com.intellij.util.xmlb.annotations.Transient;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * @author Irina.Chernushina on 4/19/2017.
+ */
+@Tag("SchemaInfo")
+public class UserDefinedJsonSchemaConfiguration {
+ private final static Comparator<Item> ITEM_COMPARATOR = (o1, o2) -> {
+ if (o1.isPattern() != o2.isPattern()) return o1.isPattern() ? -1 : 1;
+ if (o1.isDirectory() != o2.isDirectory()) return o1.isDirectory() ? -1 : 1;
+ return o1.path.compareToIgnoreCase(o2.path);
+ };
+
+ public String name;
+ public String relativePathToSchema;
+ public JsonSchemaVersion schemaVersion = JsonSchemaVersion.SCHEMA_4;
+ public boolean applicationDefined;
+ public List<Item> patterns = new SmartList<>();
+ @Transient
+ private final AtomicClearableLazyValue<List<PairProcessor<Project, VirtualFile>>> myCalculatedPatterns =
+ new AtomicClearableLazyValue<List<PairProcessor<Project, VirtualFile>>>() {
+ @NotNull
+ @Override
+ protected List<PairProcessor<Project, VirtualFile>> compute() {
+ return recalculatePatterns();
+ }
+ };
+
+ public UserDefinedJsonSchemaConfiguration() {
+ }
+
+ public UserDefinedJsonSchemaConfiguration(@NotNull String name,
+ JsonSchemaVersion schemaVersion,
+ @NotNull String relativePathToSchema,
+ boolean applicationDefined,
+ @Nullable List<Item> patterns) {
+ this.name = name;
+ this.relativePathToSchema = relativePathToSchema;
+ this.schemaVersion = schemaVersion;
+ this.applicationDefined = applicationDefined;
+ setPatterns(patterns);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(@NotNull String name) {
+ this.name = name;
+ }
+
+ public String getRelativePathToSchema() {
+ return relativePathToSchema;
+ }
+
+ public JsonSchemaVersion getSchemaVersion() {
+ return schemaVersion;
+ }
+
+ public void setSchemaVersion(JsonSchemaVersion schemaVersion) {
+ this.schemaVersion = schemaVersion;
+ }
+
+ public void setRelativePathToSchema(String relativePathToSchema) {
+ this.relativePathToSchema = relativePathToSchema;
+ }
+
+ public boolean isApplicationDefined() {
+ return applicationDefined;
+ }
+
+ public void setApplicationDefined(boolean applicationDefined) {
+ this.applicationDefined = applicationDefined;
+ }
+
+ public List<Item> getPatterns() {
+ return patterns;
+ }
+
+ public void setPatterns(@Nullable List<Item> patterns) {
+ this.patterns.clear();
+ if (patterns != null) this.patterns.addAll(patterns);
+ Collections.sort(this.patterns, ITEM_COMPARATOR);
+ myCalculatedPatterns.drop();
+ }
+
+ public void refreshPatterns() {
+ myCalculatedPatterns.drop();
+ }
+
+ @NotNull
+ public List<PairProcessor<Project, VirtualFile>> getCalculatedPatterns() {
+ return myCalculatedPatterns.getValue();
+ }
+
+ private List<PairProcessor<Project, VirtualFile>> recalculatePatterns() {
+ final List<PairProcessor<Project, VirtualFile>> result = new SmartList<>();
+ for (final Item patternText : patterns) {
+ switch (patternText.mappingKind) {
+ case File:
+ result.add((project, vfile) -> vfile.equals(getRelativeFile(project, patternText)) || vfile.getUrl().equals(patternText.getPath()));
+ break;
+ case Pattern:
+ String pathText = patternText.getPath().replace(File.separatorChar, '/').replace('\\', '/');
+ final Pattern pattern = pathText.isEmpty()
+ ? PatternUtil.NOTHING
+ : pathText.indexOf('/') >= 0
+ ? PatternUtil.compileSafe(".*/" + PatternUtil.convertToRegex(pathText), PatternUtil.NOTHING)
+ : PatternUtil.fromMask(pathText);
+ result.add((project, file) -> JsonSchemaObject.matchPattern(pattern, pathText.indexOf('/') >= 0
+ ? file.getPath()
+ : file.getName()));
+ break;
+ case Directory:
+ result.add((project, vfile) -> {
+ final VirtualFile relativeFile = getRelativeFile(project, patternText);
+ if (relativeFile == null || !VfsUtilCore.isAncestor(relativeFile, vfile, true)) return false;
+ JsonSchemaService service = JsonSchemaService.Impl.get(project);
+ return service.isApplicableToFile(vfile);
+ });
+ break;
+ }
+ }
+ return result;
+ }
+
+ @Nullable
+ private static VirtualFile getRelativeFile(@NotNull final Project project, @NotNull final Item pattern) {
+ if (project.getBasePath() == null) {
+ return null;
+ }
+
+ final String path = FileUtilRt.toSystemIndependentName(StringUtil.notNullize(pattern.path));
+ final List<String> parts = pathToPartsList(path);
+ if (parts.isEmpty()) {
+ return project.getBaseDir();
+ }
+ else {
+ return VfsUtil.findRelativeFile(project.getBaseDir(), ArrayUtil.toStringArray(parts));
+ }
+ }
+
+ @NotNull
+ private static List<String> pathToPartsList(@NotNull String path) {
+ return ContainerUtil.filter(StringUtil.split(path, "/"), s -> !".".equals(s));
+ }
+
+ @NotNull
+ private static String[] pathToParts(@NotNull String path) {
+ return ArrayUtil.toStringArray(pathToPartsList(path));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ UserDefinedJsonSchemaConfiguration info = (UserDefinedJsonSchemaConfiguration)o;
+
+ if (applicationDefined != info.applicationDefined) return false;
+ if (schemaVersion != info.schemaVersion) return false;
+ if (!Objects.equals(name, info.name)) return false;
+ if (!Objects.equals(relativePathToSchema, info.relativePathToSchema)) return false;
+
+ return Objects.equals(patterns, info.patterns);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name != null ? name.hashCode() : 0;
+ result = 31 * result + (relativePathToSchema != null ? relativePathToSchema.hashCode() : 0);
+ result = 31 * result + (applicationDefined ? 1 : 0);
+ result = 31 * result + (patterns != null ? patterns.hashCode() : 0);
+ result = 31 * result + schemaVersion.hashCode();
+ return result;
+ }
+
+ public static class Item {
+ public String path;
+ public JsonMappingKind mappingKind = JsonMappingKind.File;
+
+ public Item() {
+ }
+
+ public Item(String path, JsonMappingKind mappingKind) {
+ this.path = neutralizePath(path);
+ this.mappingKind = mappingKind;
+ }
+
+ public Item(String path, boolean isPattern, boolean isDirectory) {
+ this.path = neutralizePath(path);
+ this.mappingKind = isPattern ? JsonMappingKind.Pattern : isDirectory ? JsonMappingKind.Directory : JsonMappingKind.File;
+ }
+
+ @NotNull
+ private static String normalizePath(String path) {
+ if (preserveSlashes(path)) return path;
+ return StringUtil.trimEnd(FileUtilRt.toSystemDependentName(path), File.separatorChar);
+ }
+
+ private static boolean preserveSlashes(String path) {
+ // http/https URLs to schemas
+ // mock URLs of fragments editor
+ return StringUtil.startsWith(path, "http:")
+ || StringUtil.startsWith(path, "https:")
+ || StringUtil.startsWith(path, "mock:");
+ }
+
+ @NotNull
+ private static String neutralizePath(String path) {
+ if (preserveSlashes(path)) return path;
+ return StringUtil.trimEnd(FileUtilRt.toSystemIndependentName(path), '/');
+ }
+
+ public String getPath() {
+ return normalizePath(path);
+ }
+
+ public void setPath(String path) {
+ this.path = neutralizePath(path);
+ }
+
+ public String getError() {
+ switch (mappingKind) {
+ case File:
+ return !StringUtil.isEmpty(path) ? null : "Empty file path doesn't match anything";
+ case Pattern:
+ return !StringUtil.isEmpty(path) ? null : "Empty pattern matches nothing";
+ case Directory:
+ return null;
+ }
+
+ return "Unknown mapping kind";
+ }
+
+ public boolean isPattern() {
+ return mappingKind == JsonMappingKind.Pattern;
+ }
+
+ public void setPattern(boolean pattern) {
+ mappingKind = pattern ? JsonMappingKind.Pattern : JsonMappingKind.File;
+ }
+
+ public boolean isDirectory() {
+ return mappingKind == JsonMappingKind.Directory;
+ }
+
+ public void setDirectory(boolean directory) {
+ mappingKind = directory ? JsonMappingKind.Directory : JsonMappingKind.File;
+ }
+
+ public String getPresentation() {
+ if (mappingKind == JsonMappingKind.Directory && StringUtil.isEmpty(path)) {
+ return mappingKind.getPrefix() + "[Project Directory]";
+ }
+ return mappingKind.getPrefix() + getPath();
+ }
+
+ public String[] getPathParts() {
+ return pathToParts(path);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Item item = (Item)o;
+
+ if (mappingKind != item.mappingKind) return false;
+ return Objects.equals(path, item.path);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hashCode(path);
+ result = 31 * result + Objects.hashCode(mappingKind);
+ return result;
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalker.java b/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalker.java
new file mode 100644
index 00000000..c805a06a
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalker.java
@@ -0,0 +1,81 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.extension;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.ThreeState;
+import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import com.jetbrains.jsonSchema.impl.JsonOriginalPsiWalker;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVariantsTreeBuilder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author Irina.Chernushina on 2/15/2017.
+ */
+public interface JsonLikePsiWalker {
+ JsonOriginalPsiWalker JSON_ORIGINAL_PSI_WALKER = new JsonOriginalPsiWalker();
+
+ /**
+ * Returns YES in place where a property name is expected,
+ * NO in place where a property value is expected,
+ * UNSURE where both property name and property value can be present
+ */
+ ThreeState isName(PsiElement element);
+
+ boolean isPropertyWithValue(@NotNull PsiElement element);
+ PsiElement goUpToCheckable(@NotNull final PsiElement element);
+ @Nullable
+ List<JsonSchemaVariantsTreeBuilder.Step> findPosition(@NotNull final PsiElement element, boolean forceLastTransition);
+ boolean isNameQuoted();
+ boolean onlyDoubleQuotesForStringLiterals();
+ default boolean quotesForStringLiterals() { return true; }
+ boolean hasPropertiesBehindAndNoComma(@NotNull PsiElement element);
+ Set<String> getPropertyNamesOfParentObject(@NotNull PsiElement originalPosition, PsiElement computedPosition);
+ @Nullable
+ JsonPropertyAdapter getParentPropertyAdapter(@NotNull PsiElement element);
+ boolean isTopJsonElement(@NotNull PsiElement element);
+ @Nullable
+ JsonValueAdapter createValueAdapter(@NotNull PsiElement element);
+
+ default TextRange adjustErrorHighlightingRange(@NotNull PsiElement element) {
+ return element.getTextRange();
+ }
+
+ @Nullable
+ static JsonLikePsiWalker getWalker(@NotNull final PsiElement element, JsonSchemaObject schemaObject) {
+ if (JSON_ORIGINAL_PSI_WALKER.handles(element)) return JSON_ORIGINAL_PSI_WALKER;
+
+ return JsonLikePsiWalkerFactory.EXTENSION_POINT_NAME.getExtensionList().stream()
+ .filter(extension -> extension.handles(element))
+ .findFirst()
+ .map(extension -> extension.create(schemaObject))
+ .orElse(null);
+ }
+
+ default String getDefaultObjectValue() { return "{}"; }
+ @Nullable default String defaultObjectValueDescription() { return null; }
+ default String getDefaultArrayValue() { return "[]"; }
+ @Nullable default String defaultArrayValueDescription() { return null; }
+
+ default boolean invokeEnterBeforeObjectAndArray() { return false; }
+
+ default String getNodeTextForValidation(PsiElement element) { return element.getText(); }
+
+ default QuickFixAdapter getQuickFixAdapter(Project project) { return null; }
+ interface QuickFixAdapter {
+ @Nullable PsiElement getPropertyValue(PsiElement property);
+ default @NotNull PsiElement adjustValue(@NotNull PsiElement value) { return value; }
+ @Nullable String getPropertyName(PsiElement property);
+ @NotNull PsiElement createProperty(@NotNull final String name, @NotNull final String value);
+ boolean ensureComma(PsiElement backward, PsiElement self, PsiElement newElement);
+ void removeIfComma(PsiElement forward);
+ boolean fixWhitespaceBefore(PsiElement initialElement, PsiElement element);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalkerFactory.java b/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalkerFactory.java
new file mode 100644
index 00000000..aa803d3b
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalkerFactory.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2017 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;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.psi.PsiElement;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Irina.Chernushina on 3/7/2017.
+ */
+public interface JsonLikePsiWalkerFactory {
+ ExtensionPointName<JsonLikePsiWalkerFactory> EXTENSION_POINT_NAME = ExtensionPointName.create("com.intellij.json.jsonLikePsiWalkerFactory");
+
+ boolean handles(@NotNull PsiElement element);
+
+ @NotNull
+ JsonLikePsiWalker create(@NotNull JsonSchemaObject schemaObject);
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaEnabler.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaEnabler.java
new file mode 100644
index 00000000..f0926b2e
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaEnabler.java
@@ -0,0 +1,31 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.extension;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.vfs.VirtualFile;
+
+/**
+ * This API provides a mechanism to enable JSON schemas in particular files
+ * This interface should be implemented if you want a particular kind of virtual files to have access to JsonSchemaService APIs
+ *
+ * This API is new in IntelliJ IDEA Platform 2018.2
+ */
+public interface JsonSchemaEnabler {
+ ExtensionPointName<JsonSchemaEnabler> EXTENSION_POINT_NAME = ExtensionPointName.create("com.intellij.json.jsonSchemaEnabler");
+
+ /**
+ * This method should return true if JSON schema mechanism should become applicable to corresponding file.
+ * This method SHOULD NOT ADDRESS INDEXES.
+ * @param file Virtual file to check for
+ * @return true if available, false otherwise
+ */
+ boolean isEnabledForFile(VirtualFile file);
+
+ /**
+ * This method enables/disables JSON schema selection widget
+ * This method SHOULD NOT ADDRESS INDEXES
+ */
+ default boolean shouldShowSwitcherWidget(VirtualFile file) {
+ return true;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaFileProvider.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaFileProvider.java
new file mode 100644
index 00000000..9047afee
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaFileProvider.java
@@ -0,0 +1,37 @@
+package com.jetbrains.jsonSchema.extension;
+
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public interface JsonSchemaFileProvider {
+ boolean isAvailable(@NotNull VirtualFile file);
+
+ @NotNull
+ String getName();
+
+ @Nullable
+ VirtualFile getSchemaFile();
+
+ @NotNull
+ SchemaType getSchemaType();
+
+ default JsonSchemaVersion getSchemaVersion() {
+ return JsonSchemaVersion.SCHEMA_4;
+ }
+
+ @Nullable
+ default String getThirdPartyApiInformation() {
+ return null;
+ }
+
+ default boolean isUserVisible() { return true; }
+
+ @NotNull
+ default String getPresentableName() { return getName(); }
+
+ @Nullable
+ default String getRemoteSource() { return null; }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaImportedProviderMarker.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaImportedProviderMarker.java
new file mode 100644
index 00000000..e6bb993c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaImportedProviderMarker.java
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+/**
+ * @author Irina.Chernushina on 2/15/2016.
+ */
+public interface JsonSchemaImportedProviderMarker {
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaInfo.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaInfo.java
new file mode 100644
index 00000000..c05c5c5e
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaInfo.java
@@ -0,0 +1,132 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.extension;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.impl.JsonSchemaType;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.Set;
+
+public class JsonSchemaInfo {
+ @Nullable private final JsonSchemaFileProvider myProvider;
+ @Nullable private final String myUrl;
+ @NotNull private final static Set<String> myDumbNames = ContainerUtil.set(
+ "schema",
+ "lib",
+ "cli",
+ "packages",
+ "master",
+ "format",
+ "angular", // the only angular-related schema is the 'angular-cli', so we skip the repo name
+ "config");
+
+ public JsonSchemaInfo(@NotNull JsonSchemaFileProvider provider) {
+ myProvider = provider;
+ myUrl = null;
+ }
+
+ public JsonSchemaInfo(@NotNull String url) {
+ myUrl = url;
+ myProvider = null;
+ }
+
+ @Nullable
+ public JsonSchemaFileProvider getProvider() {
+ return myProvider;
+ }
+
+ @NotNull
+ public String getUrl(Project project) {
+ if (myProvider != null) {
+ String remoteSource = myProvider.getRemoteSource();
+ if (remoteSource != null) {
+ return remoteSource;
+ }
+
+ VirtualFile schemaFile = myProvider.getSchemaFile();
+ if (schemaFile == null) return "";
+
+ if (schemaFile instanceof HttpVirtualFile) {
+ return schemaFile.getUrl();
+ }
+
+ return getRelativePath(project, schemaFile.getPath());
+ }
+ else {
+ assert myUrl != null;
+ return myUrl;
+ }
+ }
+
+ @NotNull
+ public String getDescription() {
+ if (myProvider != null) {
+ String providerName = myProvider.getPresentableName();
+ return sanitizeName(providerName);
+ }
+
+ assert myUrl != null;
+
+ // the only weird case
+ if ("http://json.schemastore.org/config".equals(myUrl)
+ || "https://schemastore.azurewebsites.net/schemas/json/config.json".equals(myUrl)) {
+ return "asp.net config";
+ }
+
+ String url = myUrl.replace('\\', '/');
+
+ return ContainerUtil.reverse(StringUtil.split(url, "/"))
+ .stream()
+ .map(p -> sanitizeName(p))
+ .filter(p -> !isVeryDumbName(p))
+ .findFirst().orElse(sanitizeName(myUrl));
+ }
+
+ public static boolean isVeryDumbName(@Nullable String possibleName) {
+ if (StringUtil.isEmptyOrSpaces(possibleName) || myDumbNames.contains(possibleName)) return true;
+ return StringUtil.split(possibleName, ".").stream().allMatch(s -> JsonSchemaType.isInteger(s));
+ }
+
+ @NotNull
+ private static String sanitizeName(@NotNull String providerName) {
+ return StringUtil.trimEnd(StringUtil.trimEnd(StringUtil.trimEnd(providerName, ".json"), "-schema"), ".schema");
+ }
+
+ @NotNull
+ public JsonSchemaVersion getSchemaVersion() {
+ return myProvider != null ? myProvider.getSchemaVersion() : JsonSchemaVersion.SCHEMA_4;
+ }
+
+ @NotNull
+ public static String getRelativePath(@NotNull Project project, @NotNull String text) {
+ text = text.trim();
+ if (project.isDefault() || project.getBasePath() == null) return text;
+ if (StringUtil.isEmptyOrSpaces(text)) return text;
+ final File ioFile = new File(text);
+ if (!ioFile.isAbsolute()) return text;
+ VirtualFile file = VfsUtil.findFileByIoFile(ioFile, false);
+ if (file == null) return text;
+ final String relativePath = VfsUtilCore.getRelativePath(file, project.getBaseDir());
+ if (relativePath != null) return relativePath;
+ if (isMeaningfulAncestor(VfsUtilCore.getCommonAncestor(file, project.getBaseDir()))) {
+ String path = VfsUtilCore.findRelativePath(project.getBaseDir(), file, File.separatorChar);
+ if (path != null) return path;
+ }
+ return text;
+ }
+
+ private static boolean isMeaningfulAncestor(@Nullable VirtualFile ancestor) {
+ if (ancestor == null) return false;
+ VirtualFile homeDir = VfsUtil.getUserHomeDir();
+ return homeDir != null && VfsUtilCore.isAncestor(homeDir, ancestor, true);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProjectSelfProviderFactory.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProjectSelfProviderFactory.java
new file mode 100644
index 00000000..829c8c50
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProjectSelfProviderFactory.java
@@ -0,0 +1,125 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.extension;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.NullableLazyValue;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import kotlin.NotImplementedError;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * @author Irina.Chernushina on 2/24/2016.
+ */
+public class JsonSchemaProjectSelfProviderFactory implements JsonSchemaProviderFactory {
+ public static final int TOTAL_PROVIDERS = 3;
+ private static final String SCHEMA_JSON_FILE_NAME = "schema.json";
+ private static final String SCHEMA06_JSON_FILE_NAME = "schema06.json";
+ private static final String SCHEMA07_JSON_FILE_NAME = "schema07.json";
+
+ @NotNull
+ @Override
+ public List<JsonSchemaFileProvider> getProviders(@NotNull final Project project) {
+ return ContainerUtil.list(new MyJsonSchemaFileProvider(project, SCHEMA_JSON_FILE_NAME),
+ new MyJsonSchemaFileProvider(project, SCHEMA06_JSON_FILE_NAME),
+ new MyJsonSchemaFileProvider(project, SCHEMA07_JSON_FILE_NAME));
+ }
+
+ public static class MyJsonSchemaFileProvider implements JsonSchemaFileProvider {
+ @NotNull private final Project myProject;
+ @NotNull private final NullableLazyValue<VirtualFile> mySchemaFile;
+ @NotNull private final String myFileName;
+
+ public boolean isSchemaV4() {
+ return SCHEMA_JSON_FILE_NAME.equals(myFileName);
+ }
+ public boolean isSchemaV6() {
+ return SCHEMA06_JSON_FILE_NAME.equals(myFileName);
+ }
+ public boolean isSchemaV7() {
+ return SCHEMA07_JSON_FILE_NAME.equals(myFileName);
+ }
+
+ private MyJsonSchemaFileProvider(@NotNull final Project project, @NotNull String fileName) {
+ myProject = project;
+ myFileName = fileName;
+ // schema file can not be static here, because in schema's user data we cache project-scope objects (i.e. which can refer to project)
+ mySchemaFile = NullableLazyValue.createValue(() -> JsonSchemaProviderFactory.getResourceFile(JsonSchemaProjectSelfProviderFactory.class, "/jsonSchema/" + fileName));
+ }
+
+ @Override
+ public boolean isAvailable(@NotNull VirtualFile file) {
+ if (myProject.isDisposed()) return false;
+ JsonSchemaService service = JsonSchemaService.Impl.get(myProject);
+ if (!service.isApplicableToFile(file)) return false;
+ JsonSchemaVersion schemaVersion = service.getSchemaVersion(file);
+ if (schemaVersion == null) return false;
+ switch (schemaVersion) {
+ case SCHEMA_4:
+ return isSchemaV4();
+ case SCHEMA_6:
+ return isSchemaV6();
+ case SCHEMA_7:
+ return isSchemaV7();
+ }
+
+ throw new NotImplementedError("Unknown schema version: " + schemaVersion);
+ }
+
+ @Override
+ public JsonSchemaVersion getSchemaVersion() {
+ return isSchemaV4() ? JsonSchemaVersion.SCHEMA_4 : isSchemaV7() ? JsonSchemaVersion.SCHEMA_7 : JsonSchemaVersion.SCHEMA_6;
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return myFileName;
+ }
+
+ @Nullable
+ @Override
+ public VirtualFile getSchemaFile() {
+ return mySchemaFile.getValue();
+ }
+
+ @NotNull
+ @Override
+ public SchemaType getSchemaType() {
+ return SchemaType.schema;
+ }
+
+ @Nullable
+ @Override
+ public String getRemoteSource() {
+ switch (myFileName) {
+ case SCHEMA_JSON_FILE_NAME:
+ return "http://json-schema.org/draft-04/schema";
+ case SCHEMA06_JSON_FILE_NAME:
+ return "http://json-schema.org/draft-06/schema";
+ case SCHEMA07_JSON_FILE_NAME:
+ return "http://json-schema.org/draft-07/schema";
+ }
+ return null;
+ }
+
+ @NotNull
+ @Override
+ public String getPresentableName() {
+ switch (myFileName) {
+ case SCHEMA_JSON_FILE_NAME:
+ return "JSON schema v4";
+ case SCHEMA06_JSON_FILE_NAME:
+ return "JSON schema v6";
+ case SCHEMA07_JSON_FILE_NAME:
+ return "JSON schema v7";
+ }
+ return getName();
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProviderFactory.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProviderFactory.java
new file mode 100644
index 00000000..70df36dd
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProviderFactory.java
@@ -0,0 +1,42 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.extension;
+
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+
+import java.net.URL;
+import java.util.List;
+
+public interface JsonSchemaProviderFactory {
+ ExtensionPointName<JsonSchemaProviderFactory> EP_NAME = ExtensionPointName.create("JavaScript.JsonSchema.ProviderFactory");
+ Logger LOG = Logger.getInstance(JsonSchemaProviderFactory.class);
+
+ @NotNull
+ List<JsonSchemaFileProvider> getProviders(@NotNull Project project);
+
+ /**
+ * Finds a {@link VirtualFile} instance corresponding to a specified resource path (relative or absolute).
+ *
+ * @param baseClass
+ * @param resourcePath String identifying a resource (relative or absolute)
+ * See {@link Class#getResource(String)} for more details
+ * @return VirtualFile instance, or null if not found
+ */
+ static VirtualFile getResourceFile(@NotNull Class baseClass, @NotNull String resourcePath) {
+ URL url = baseClass.getResource(resourcePath);
+ if (url == null) {
+ LOG.error("Cannot find resource " + resourcePath);
+ return null;
+ }
+ VirtualFile file = VfsUtil.findFileByURL(url);
+ if (file == null) {
+ LOG.error("Cannot find file by " + resourcePath);
+ return null;
+ }
+ return file;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaUserDefinedProviderFactory.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaUserDefinedProviderFactory.java
new file mode 100644
index 00000000..09117dd5
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaUserDefinedProviderFactory.java
@@ -0,0 +1,138 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.extension;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.PairProcessor;
+import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import com.jetbrains.jsonSchema.remote.JsonFileResolver;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static com.jetbrains.jsonSchema.remote.JsonFileResolver.isHttpPath;
+
+/**
+ * @author Irina.Chernushina on 2/13/2016.
+ */
+public class JsonSchemaUserDefinedProviderFactory implements JsonSchemaProviderFactory {
+ @NotNull
+ @Override
+ public List<JsonSchemaFileProvider> getProviders(@NotNull Project project) {
+ final JsonSchemaMappingsProjectConfiguration configuration = JsonSchemaMappingsProjectConfiguration.getInstance(project);
+
+ final Map<String, UserDefinedJsonSchemaConfiguration> map = configuration.getStateMap();
+ final List<JsonSchemaFileProvider> providers = map.values().stream()
+ .map(schema -> createProvider(project, schema)).collect(Collectors.toList());
+
+ return providers.isEmpty() ? Collections.emptyList() : providers;
+ }
+
+ @NotNull
+ public MyProvider createProvider(@NotNull Project project,
+ UserDefinedJsonSchemaConfiguration schema) {
+ String relPath = schema.getRelativePathToSchema();
+ return new MyProvider(project, schema.getSchemaVersion(), schema.getName(),
+ isHttpPath(relPath) || new File(relPath).isAbsolute()
+ ? relPath
+ : new File(project.getBasePath(),
+ relPath).getAbsolutePath(),
+ schema.getCalculatedPatterns());
+ }
+
+ static class MyProvider implements JsonSchemaFileProvider, JsonSchemaImportedProviderMarker {
+ @NotNull private final Project myProject;
+ @NotNull private final JsonSchemaVersion myVersion;
+ @NotNull private final String myName;
+ @NotNull private final String myFile;
+ private VirtualFile myVirtualFile;
+ @NotNull private final List<? extends PairProcessor<Project, VirtualFile>> myPatterns;
+
+ MyProvider(@NotNull final Project project,
+ @NotNull final JsonSchemaVersion version,
+ @NotNull final String name,
+ @NotNull final String file,
+ @NotNull final List<? extends PairProcessor<Project, VirtualFile>> patterns) {
+ myProject = project;
+ myVersion = version;
+ myName = name;
+ myFile = file;
+ myPatterns = patterns;
+ }
+
+ @Override
+ public JsonSchemaVersion getSchemaVersion() {
+ return myVersion;
+ }
+
+ @Nullable
+ @Override
+ public VirtualFile getSchemaFile() {
+ if (myVirtualFile != null && myVirtualFile.isValid()) return myVirtualFile;
+ String path = myFile;
+ if (isHttpPath(path)) {
+ myVirtualFile = JsonFileResolver.urlToFile(path);
+ }
+ else {
+ final LocalFileSystem lfs = LocalFileSystem.getInstance();
+ myVirtualFile = lfs.findFileByPath(myFile);
+ if (myVirtualFile == null) {
+ myVirtualFile = lfs.refreshAndFindFileByPath(myFile);
+ }
+ }
+ return myVirtualFile;
+ }
+
+ @NotNull
+ @Override
+ public SchemaType getSchemaType() {
+ return SchemaType.userSchema;
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return myName;
+ }
+
+ @Override
+ public boolean isAvailable(@NotNull VirtualFile file) {
+ //noinspection SimplifiableIfStatement
+ if (myPatterns.isEmpty() || file.isDirectory() || !file.isValid() || getSchemaFile() == null) return false;
+ return myPatterns.stream().anyMatch(processor -> processor.process(myProject, file));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ MyProvider provider = (MyProvider)o;
+
+ if (!myName.equals(provider.myName)) return false;
+ return FileUtil.pathsEqual(myFile, provider.myFile);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = myName.hashCode();
+ result = 31 * result + FileUtil.pathHashCode(myFile);
+ return result;
+ }
+
+ @Nullable
+ @Override
+ public String getRemoteSource() {
+ return isHttpPath(myFile) ? myFile : null;
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonWidgetSuppressor.java b/json/src/com/jetbrains/jsonSchema/extension/JsonWidgetSuppressor.java
new file mode 100644
index 00000000..54dc903f
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/JsonWidgetSuppressor.java
@@ -0,0 +1,17 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.extension;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+
+public interface JsonWidgetSuppressor {
+ ExtensionPointName<JsonWidgetSuppressor> EXTENSION_POINT_NAME = ExtensionPointName.create("com.intellij.json.jsonWidgetSuppressor");
+
+ /**
+ * Allows to suppress JSON widget for particular files
+ * This method can access indexes and PSI
+ */
+ boolean suppressSwitcherWidget(@NotNull VirtualFile file, @NotNull Project project);
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/SchemaType.java b/json/src/com/jetbrains/jsonSchema/extension/SchemaType.java
new file mode 100644
index 00000000..b59e5a1c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/SchemaType.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+/**
+ * @author Irina.Chernushina on 3/29/2016.
+ */
+public enum SchemaType {
+ schema, embeddedSchema, userSchema, remoteSchema
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonArrayValueAdapter.java b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonArrayValueAdapter.java
new file mode 100644
index 00000000..af247f78
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonArrayValueAdapter.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2000-2017 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.adapters;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public interface JsonArrayValueAdapter extends JsonValueAdapter {
+ @NotNull List<JsonValueAdapter> getElements();
+
+ @Override
+ default boolean isNull() {
+ return false;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonObjectValueAdapter.java b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonObjectValueAdapter.java
new file mode 100644
index 00000000..627ddf56
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonObjectValueAdapter.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2000-2017 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.adapters;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public interface JsonObjectValueAdapter extends JsonValueAdapter {
+ @NotNull List<JsonPropertyAdapter> getPropertyList();
+
+ @Override
+ default boolean isNull() {
+ return false;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonPropertyAdapter.java b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonPropertyAdapter.java
new file mode 100644
index 00000000..e82279bf
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonPropertyAdapter.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2017 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.adapters;
+
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public interface JsonPropertyAdapter {
+ @Nullable String getName();
+ @Nullable JsonValueAdapter getNameValueAdapter();
+ @NotNull Collection<JsonValueAdapter> getValues();
+ @NotNull PsiElement getDelegate();
+ @Nullable JsonObjectValueAdapter getParentObject();
+}
diff --git a/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonValueAdapter.java b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonValueAdapter.java
new file mode 100644
index 00000000..1d8f2742
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonValueAdapter.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2000-2017 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.adapters;
+
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public interface JsonValueAdapter {
+ default boolean isShouldBeIgnored() {return false;}
+ boolean isObject();
+ boolean isArray();
+ boolean isStringLiteral();
+ boolean isNumberLiteral();
+ boolean isBooleanLiteral();
+ boolean isNull();
+
+ @NotNull PsiElement getDelegate();
+
+ @Nullable JsonObjectValueAdapter getAsObject();
+ @Nullable JsonArrayValueAdapter getAsArray();
+
+ default boolean shouldCheckIntegralRequirements() {return true;}
+}
diff --git a/json/src/com/jetbrains/jsonSchema/ide/JsonSchemaService.java b/json/src/com/jetbrains/jsonSchema/ide/JsonSchemaService.java
new file mode 100644
index 00000000..2070e3fc
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/ide/JsonSchemaService.java
@@ -0,0 +1,75 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.ide;
+
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.ModificationTracker;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider;
+import com.jetbrains.jsonSchema.extension.JsonSchemaInfo;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+
+public interface JsonSchemaService {
+ class Impl {
+ public static JsonSchemaService get(@NotNull Project project) {
+ return ServiceManager.getService(project, JsonSchemaService.class);
+ }
+ }
+
+ static boolean isSchemaFile(@NotNull PsiFile psiFile) {
+ final VirtualFile file = psiFile.getViewProvider().getVirtualFile();
+ JsonSchemaService service = Impl.get(psiFile.getProject());
+ return service.isSchemaFile(file) && service.isApplicableToFile(file);
+ }
+
+ boolean isSchemaFile(@NotNull VirtualFile file);
+
+ @Nullable
+ JsonSchemaVersion getSchemaVersion(@NotNull VirtualFile file);
+
+ @NotNull
+ Collection<VirtualFile> getSchemaFilesForFile(@NotNull VirtualFile file);
+
+ void registerRemoteUpdateCallback(Runnable callback);
+ void unregisterRemoteUpdateCallback(Runnable callback);
+ void registerResetAction(Runnable action);
+ void unregisterResetAction(Runnable action);
+
+ void registerReference(String ref);
+ boolean possiblyHasReference(String ref);
+
+ void triggerUpdateRemote();
+
+ @Nullable
+ JsonSchemaObject getSchemaObject(@NotNull VirtualFile file);
+
+ @Nullable
+ JsonSchemaObject getSchemaObjectForSchemaFile(@NotNull VirtualFile schemaFile);
+
+ @Nullable
+ VirtualFile findSchemaFileByReference(@NotNull String reference, @Nullable VirtualFile referent);
+
+ @Nullable
+ JsonSchemaFileProvider getSchemaProvider(@NotNull final VirtualFile schemaFile);
+
+ void reset();
+
+ ModificationTracker getAnySchemaChangeTracker();
+
+ List<JsonSchemaInfo> getAllUserVisibleSchemas();
+
+ boolean isApplicableToFile(@Nullable VirtualFile file);
+
+ @NotNull
+ static String normalizeId(@NotNull String id) {
+ id = id.endsWith("#") ? id.substring(0, id.length() - 1) : id;
+ return id.startsWith("#") ? id.substring(1) : id;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/CachedValueProviderOnPsiFile.java b/json/src/com/jetbrains/jsonSchema/impl/CachedValueProviderOnPsiFile.java
new file mode 100644
index 00000000..b069841a
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/CachedValueProviderOnPsiFile.java
@@ -0,0 +1,41 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.util.Key;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.util.CachedValue;
+import com.intellij.psi.util.CachedValueProvider;
+import com.intellij.psi.util.CachedValuesManager;
+import com.intellij.util.Function;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public abstract class CachedValueProviderOnPsiFile<T> implements CachedValueProvider<T> {
+ private final PsiFile myPsiFile;
+
+ public CachedValueProviderOnPsiFile(@NotNull PsiFile psiFile) {
+ myPsiFile = psiFile;
+ }
+
+ @Nullable
+ @Override
+ public Result<T> compute() {
+ return CachedValueProvider.Result.create(evaluate(myPsiFile), myPsiFile);
+ }
+
+ @Nullable
+ public abstract T evaluate(@NotNull PsiFile psiFile);
+
+ @Nullable
+ public static <T> T getOrCompute(@NotNull PsiFile psiFile, @NotNull Function<? super PsiFile, ? extends T> eval, @NotNull Key<CachedValue<T>> key) {
+ final CachedValueProvider<T> provider = new CachedValueProviderOnPsiFile<T>(psiFile) {
+ @Override
+ @Nullable
+ public T evaluate(@NotNull PsiFile psiFile) {
+ return eval.fun(psiFile);
+ }
+ };
+ return ReadAction.compute(() -> CachedValuesManager.getCachedValue(psiFile, key, provider));
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/EnumArrayValueWrapper.java b/json/src/com/jetbrains/jsonSchema/impl/EnumArrayValueWrapper.java
new file mode 100644
index 00000000..70f66a56
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/EnumArrayValueWrapper.java
@@ -0,0 +1,25 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+public class EnumArrayValueWrapper {
+ @NotNull private final Object[] myValues;
+
+ public EnumArrayValueWrapper(@NotNull Object[] values) {
+ myValues = values;
+ }
+
+ @NotNull
+ public Object[] getValues() {
+ return myValues;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + Arrays.stream(myValues).map(v -> v.toString()).collect(Collectors.joining(", ")) + "]";
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/EnumObjectValueWrapper.java b/json/src/com/jetbrains/jsonSchema/impl/EnumObjectValueWrapper.java
new file mode 100644
index 00000000..b8da4134
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/EnumObjectValueWrapper.java
@@ -0,0 +1,25 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class EnumObjectValueWrapper {
+ @NotNull private final Map<String, Object> myValues;
+
+ public EnumObjectValueWrapper(@NotNull Map<String, Object> values) {
+ myValues = values;
+ }
+
+ @NotNull
+ public Map<String, Object> getValues() {
+ return myValues;
+ }
+
+ @Override
+ public String toString() {
+ return "{" + myValues.entrySet().stream().map(v -> "\"" + v.getKey() + "\": " + v.getValue()).collect(Collectors.joining(", ")) + "}";
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonCachedValues.java b/json/src/com/jetbrains/jsonSchema/impl/JsonCachedValues.java
new file mode 100644
index 00000000..40773ae4
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonCachedValues.java
@@ -0,0 +1,192 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.navigation.JsonQualifiedNameKind;
+import com.intellij.json.navigation.JsonQualifiedNameProvider;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.SyntaxTraverser;
+import com.intellij.psi.util.CachedValue;
+import com.intellij.util.AstLoadingFilter;
+import com.intellij.util.Function;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.remote.JsonFileResolver;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class JsonCachedValues {
+ private static final Key<CachedValue<JsonSchemaObject>> JSON_OBJECT_CACHE_KEY = Key.create("JsonSchemaObjectCache");
+
+ @Nullable
+ public static JsonSchemaObject getSchemaObject(@NotNull VirtualFile schemaFile, @NotNull Project project) {
+ JsonFileResolver.startFetchingHttpFileIfNeeded(schemaFile, project);
+ return computeForFile(schemaFile, project, JsonCachedValues::computeSchemaObject, JSON_OBJECT_CACHE_KEY);
+ }
+
+ @Nullable
+ private static JsonSchemaObject computeSchemaObject(@NotNull PsiFile f) {
+ final JsonObject topLevelValue = AstLoadingFilter.forceAllowTreeLoading(
+ f,
+ () -> ObjectUtils.tryCast(((JsonFile)f).getTopLevelValue(), JsonObject.class));
+ if (topLevelValue != null) {
+ return new JsonSchemaReader().read(topLevelValue);
+ }
+ return null;
+ }
+
+ static final String URL_CACHE_KEY = "JsonSchemaUrlCache";
+ private static final Key<CachedValue<String>> SCHEMA_URL_KEY = Key.create(URL_CACHE_KEY);
+ @Nullable
+ public static String getSchemaUrlFromSchemaProperty(@NotNull VirtualFile file,
+ @NotNull Project project) {
+ String value = JsonSchemaFileValuesIndex.getCachedValue(project, file, URL_CACHE_KEY);
+ if (value != null) {
+ return JsonSchemaFileValuesIndex.NULL.equals(value) ? null : value;
+ }
+
+ PsiFile psiFile = resolveFile(file, project);
+ return !(psiFile instanceof JsonFile) ? null : CachedValueProviderOnPsiFile
+ .getOrCompute(psiFile, JsonCachedValues::fetchSchemaUrl, SCHEMA_URL_KEY);
+ }
+
+ private static PsiFile resolveFile(@NotNull VirtualFile file,
+ @NotNull Project project) {
+ if (project.isDisposed() || !file.isValid()) return null;
+ return PsiManager.getInstance(project).findFile(file);
+ }
+
+ @Nullable
+ static String fetchSchemaUrl(@Nullable PsiFile psiFile) {
+ if (!(psiFile instanceof JsonFile)) return null;
+ final String url = JsonSchemaFileValuesIndex.readTopLevelProps(psiFile.getFileType(), psiFile.getText()).get(URL_CACHE_KEY);
+ return url == null || JsonSchemaFileValuesIndex.NULL.equals(url) ? null : url;
+ }
+
+ static final String ID_CACHE_KEY = "JsonSchemaIdCache";
+ static final String OBSOLETE_ID_CACHE_KEY = "JsonSchemaObsoleteIdCache";
+ private static final Key<CachedValue<String>> SCHEMA_ID_CACHE_KEY = Key.create(ID_CACHE_KEY);
+ @Nullable
+ public static String getSchemaId(@NotNull final VirtualFile schemaFile,
+ @NotNull final Project project) {
+ String value = JsonSchemaFileValuesIndex.getCachedValue(project, schemaFile, ID_CACHE_KEY);
+ if (value != null && !JsonSchemaFileValuesIndex.NULL.equals(value)) return JsonSchemaService.normalizeId(value);
+ String obsoleteValue = JsonSchemaFileValuesIndex.getCachedValue(project, schemaFile, OBSOLETE_ID_CACHE_KEY);
+ if (obsoleteValue != null && !JsonSchemaFileValuesIndex.NULL.equals(obsoleteValue)) return JsonSchemaService.normalizeId(obsoleteValue);
+ if (JsonSchemaFileValuesIndex.NULL.equals(value) || JsonSchemaFileValuesIndex.NULL.equals(obsoleteValue)) return null;
+
+ final String result = computeForFile(schemaFile, project, JsonCachedValues::fetchSchemaId, SCHEMA_ID_CACHE_KEY);
+ return result == null ? null : JsonSchemaService.normalizeId(result);
+ }
+
+ @Nullable
+ private static <T> T computeForFile(@NotNull final VirtualFile schemaFile,
+ @NotNull final Project project,
+ @NotNull Function<? super PsiFile, ? extends T> eval,
+ @NotNull Key<CachedValue<T>> cacheKey) {
+ final PsiFile psiFile = resolveFile(schemaFile, project);
+ if (!(psiFile instanceof JsonFile)) return null;
+ return CachedValueProviderOnPsiFile.getOrCompute(psiFile, eval, cacheKey);
+ }
+
+ static final String ID_PATHS_CACHE_KEY = "JsonSchemaIdToPointerCache";
+ private static final Key<CachedValue<Map<String, String>>> SCHEMA_ID_PATHS_CACHE_KEY = Key.create(ID_PATHS_CACHE_KEY);
+ public static Collection<String> getAllIdsInFile(PsiFile psiFile) {
+ final Map<String, String> map = CachedValueProviderOnPsiFile.getOrCompute(psiFile, JsonCachedValues::computeIdsMap, SCHEMA_ID_PATHS_CACHE_KEY);
+ return map == null ? ContainerUtil.emptyList() : map.keySet();
+ }
+ @Nullable
+ public static String resolveId(PsiFile psiFile, String id) {
+ final Map<String, String> map = CachedValueProviderOnPsiFile.getOrCompute(psiFile, JsonCachedValues::computeIdsMap, SCHEMA_ID_PATHS_CACHE_KEY);
+ return map == null ? null : map.get(id);
+ }
+
+ private static Map<String, String> computeIdsMap(PsiFile file) {
+ return SyntaxTraverser.psiTraverser(file).filter(JsonProperty.class).filter(p -> "$id".equals(p.getName()))
+ .filter(p -> p.getValue() instanceof JsonStringLiteral)
+ .toMap(p -> ((JsonStringLiteral)Objects.requireNonNull(p.getValue())).getValue(),
+ p -> JsonQualifiedNameProvider.generateQualifiedName(p.getParent(), JsonQualifiedNameKind.JsonPointer));
+ }
+
+ @Nullable
+ static String fetchSchemaId(@NotNull PsiFile psiFile) {
+ if (!(psiFile instanceof JsonFile)) return null;
+ final Map<String, String> props = JsonSchemaFileValuesIndex.readTopLevelProps(psiFile.getFileType(), psiFile.getText());
+ final String id = props.get(ID_CACHE_KEY);
+ if (id != null && !JsonSchemaFileValuesIndex.NULL.equals(id)) return id;
+ final String obsoleteId = props.get(OBSOLETE_ID_CACHE_KEY);
+ return obsoleteId == null || JsonSchemaFileValuesIndex.NULL.equals(obsoleteId) ? null : obsoleteId;
+ }
+
+
+ private static final Key<CachedValue<List<Pair<Collection<String>, String>>>> SCHEMA_CATALOG_CACHE_KEY = Key.create("JsonSchemaCatalogCache");
+ @Nullable
+ public static List<Pair<Collection<String>, String>> getSchemaCatalog(@NotNull final VirtualFile catalog,
+ @NotNull final Project project) {
+ if (!catalog.isValid()) return null;
+ return computeForFile(catalog, project, JsonCachedValues::computeSchemaCatalog, SCHEMA_CATALOG_CACHE_KEY);
+ }
+
+ private static List<Pair<Collection<String>, String>> computeSchemaCatalog(PsiFile catalog) {
+ if (!catalog.isValid()) return null;
+ JsonValue value = AstLoadingFilter.forceAllowTreeLoading(catalog, () -> ((JsonFile)catalog).getTopLevelValue());
+ if (!(value instanceof JsonObject)) return null;
+
+ JsonProperty schemas = ((JsonObject)value).findProperty("schemas");
+ if (schemas == null) return null;
+
+ JsonValue schemasValue = schemas.getValue();
+ if (!(schemasValue instanceof JsonArray)) return null;
+ List<Pair<Collection<String>, String>> catalogMap = ContainerUtil.newArrayList();
+ fillMap((JsonArray)schemasValue, catalogMap);
+ return catalogMap;
+ }
+
+ private static void fillMap(@NotNull JsonArray array, @NotNull List<Pair<Collection<String>, String>> catalogMap) {
+ for (JsonValue value: array.getValueList()) {
+ if (!(value instanceof JsonObject)) continue;
+ JsonProperty fileMatch = ((JsonObject)value).findProperty("fileMatch");
+ Collection<String> masks = fileMatch == null ? ContainerUtil.emptyList() : resolveMasks(fileMatch.getValue());
+ JsonProperty url = ((JsonObject)value).findProperty("url");
+ if (url == null) continue;
+ JsonValue urlValue = url.getValue();
+ if (urlValue instanceof JsonStringLiteral) {
+ String urlStringValue = ((JsonStringLiteral)urlValue).getValue();
+ if (!StringUtil.isEmpty(urlStringValue)) {
+ catalogMap.add(Pair.create(masks, urlStringValue));
+ }
+ }
+ }
+ }
+
+ @NotNull
+ private static Collection<String> resolveMasks(@Nullable JsonValue value) {
+ if (value instanceof JsonStringLiteral) {
+ return ContainerUtil.createMaybeSingletonList(((JsonStringLiteral)value).getValue());
+ }
+
+ if (value instanceof JsonArray) {
+ List<String> strings = ContainerUtil.newArrayList();
+ for (JsonValue val: ((JsonArray)value).getValueList()) {
+ if (val instanceof JsonStringLiteral) {
+ strings.add(((JsonStringLiteral)val).getValue());
+ }
+ }
+ return strings;
+ }
+
+ return ContainerUtil.emptyList();
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonComplianceCheckerOptions.java b/json/src/com/jetbrains/jsonSchema/impl/JsonComplianceCheckerOptions.java
new file mode 100644
index 00000000..0ef6403c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonComplianceCheckerOptions.java
@@ -0,0 +1,13 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+public class JsonComplianceCheckerOptions {
+ public static final JsonComplianceCheckerOptions RELAX_ENUM_CHECK = new JsonComplianceCheckerOptions(true);
+
+ private final boolean isCaseInsensitiveEnumCheck;
+ public JsonComplianceCheckerOptions(boolean caseInsensitiveEnumCheck) {isCaseInsensitiveEnumCheck = caseInsensitiveEnumCheck;}
+
+ public boolean isCaseInsensitiveEnumCheck() {
+ return isCaseInsensitiveEnumCheck;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonErrorPriority.java b/json/src/com/jetbrains/jsonSchema/impl/JsonErrorPriority.java
new file mode 100644
index 00000000..660ae3bd
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonErrorPriority.java
@@ -0,0 +1,10 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+public enum JsonErrorPriority {
+ NOT_SCHEMA,
+ TYPE_MISMATCH,
+ MEDIUM_PRIORITY,
+ MISSING_PROPS,
+ LOW_PRIORITY
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonOriginalPsiWalker.java b/json/src/com/jetbrains/jsonSchema/impl/JsonOriginalPsiWalker.java
new file mode 100644
index 00000000..1262aa5c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonOriginalPsiWalker.java
@@ -0,0 +1,217 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInsight.completion.CompletionUtil;
+import com.intellij.json.JsonDialectUtil;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.impl.source.tree.LeafPsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.ThreeState;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import com.jetbrains.jsonSchema.impl.adapters.JsonJsonPropertyAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 2/16/2017.
+ */
+public class JsonOriginalPsiWalker implements JsonLikePsiWalker {
+ public static final JsonOriginalPsiWalker INSTANCE = new JsonOriginalPsiWalker();
+
+ public boolean handles(@NotNull PsiElement element) {
+ PsiElement parent = element.getParent();
+ return parent != null && (element instanceof JsonElement || element instanceof LeafPsiElement && parent instanceof JsonElement)
+ && JsonDialectUtil.isStandardJson(CompletionUtil.getOriginalOrSelf(parent));
+ }
+
+ @Override
+ public ThreeState isName(PsiElement element) {
+ final PsiElement parent = element.getParent();
+ if (parent instanceof JsonObject) {
+ return ThreeState.YES;
+ } else if (parent instanceof JsonProperty) {
+ return PsiTreeUtil.isAncestor(((JsonProperty)parent).getNameElement(), element, false) ? ThreeState.YES : ThreeState.NO;
+ }
+ return ThreeState.NO;
+ }
+
+ @Override
+ public boolean isPropertyWithValue(@NotNull PsiElement element) {
+ return element instanceof JsonProperty && ((JsonProperty)element).getValue() != null;
+ }
+
+ @Override
+ public PsiElement goUpToCheckable(@NotNull PsiElement element) {
+ PsiElement current = element;
+ while (current != null && !(current instanceof PsiFile)) {
+ if (current instanceof JsonValue || current instanceof JsonProperty) {
+ return current;
+ }
+ current = current.getParent();
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public List<JsonSchemaVariantsTreeBuilder.Step> findPosition(@NotNull PsiElement element, boolean forceLastTransition) {
+ final List<JsonSchemaVariantsTreeBuilder.Step> steps = new ArrayList<>();
+ PsiElement current = element;
+ while (! (current instanceof PsiFile)) {
+ final PsiElement position = current;
+ current = current.getParent();
+ if (current instanceof JsonArray) {
+ JsonArray array = (JsonArray)current;
+ final List<JsonValue> list = array.getValueList();
+ int idx = -1;
+ for (int i = 0; i < list.size(); i++) {
+ final JsonValue value = list.get(i);
+ if (value.equals(position)) {
+ idx = i;
+ break;
+ }
+ }
+ steps.add(JsonSchemaVariantsTreeBuilder.Step.createArrayElementStep(idx));
+ } else if (current instanceof JsonProperty) {
+ final String propertyName = ((JsonProperty)current).getName();
+ current = current.getParent();
+ if (!(current instanceof JsonObject)) return null;//incorrect syntax?
+ // if either value or not first in the chain - needed for completion variant
+ if (position != element || forceLastTransition) {
+ steps.add(JsonSchemaVariantsTreeBuilder.Step.createPropertyStep(propertyName));
+ }
+ } else if (current instanceof JsonObject && position instanceof JsonProperty) {
+ // if either value or not first in the chain - needed for completion variant
+ if (position != element || forceLastTransition) {
+ final String propertyName = ((JsonProperty)position).getName();
+ steps.add(JsonSchemaVariantsTreeBuilder.Step.createPropertyStep(propertyName));
+ }
+ } else if (current instanceof PsiFile) {
+ break;
+ } else {
+ return null;//something went wrong
+ }
+ }
+ Collections.reverse(steps);
+ return steps;
+ }
+
+ @Override
+ public boolean isNameQuoted() {
+ return true;
+ }
+
+ @Override
+ public boolean onlyDoubleQuotesForStringLiterals() {
+ return true;
+ }
+
+ @Override
+ public boolean hasPropertiesBehindAndNoComma(@NotNull PsiElement element) {
+ PsiElement current = element instanceof JsonProperty ? element : PsiTreeUtil.getParentOfType(element, JsonProperty.class);
+ while (current != null && current.getNode().getElementType() != JsonElementTypes.COMMA) {
+ current = current.getNextSibling();
+ }
+ int commaOffset = current == null ? Integer.MAX_VALUE : current.getTextRange().getStartOffset();
+ final int offset = element.getTextRange().getStartOffset();
+ final JsonObject object = PsiTreeUtil.getParentOfType(element, JsonObject.class);
+ if (object != null) {
+ for (JsonProperty property : object.getPropertyList()) {
+ final int pOffset = property.getTextRange().getStartOffset();
+ if (pOffset >= offset && !PsiTreeUtil.isAncestor(property, element, false)) {
+ return pOffset < commaOffset;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Set<String> getPropertyNamesOfParentObject(@NotNull PsiElement originalPosition, PsiElement computedPosition) {
+ final JsonObject object = PsiTreeUtil.getParentOfType(originalPosition, JsonObject.class);
+ if (object != null) {
+ return object.getPropertyList().stream()
+ .filter(p -> !isNameQuoted() || p.getNameElement() instanceof JsonStringLiteral)
+ .map(p -> StringUtil.unquoteString(p.getName())).collect(Collectors.toSet());
+ }
+ return Collections.emptySet();
+ }
+
+ @Override
+ public JsonPropertyAdapter getParentPropertyAdapter(@NotNull PsiElement element) {
+ final JsonProperty property = PsiTreeUtil.getParentOfType(element, JsonProperty.class, false);
+ if (property == null) return null;
+ return new JsonJsonPropertyAdapter(property);
+ }
+
+ @Override
+ public boolean isTopJsonElement(@NotNull PsiElement element) {
+ return element instanceof PsiFile;
+ }
+
+ @Nullable
+ @Override
+ public JsonValueAdapter createValueAdapter(@NotNull PsiElement element) {
+ return element instanceof JsonValue ? JsonJsonPropertyAdapter.createAdapterByType((JsonValue)element) : null;
+ }
+
+ @Override
+ public QuickFixAdapter getQuickFixAdapter(Project project) {
+ return new QuickFixAdapter() {
+ private final JsonElementGenerator myGenerator = new JsonElementGenerator(project);
+ @Nullable
+ @Override
+ public PsiElement getPropertyValue(PsiElement property) {
+ assert property instanceof JsonProperty;
+ return ((JsonProperty)property).getValue();
+ }
+
+ @NotNull
+ @Override
+ public String getPropertyName(PsiElement property) {
+ assert property instanceof JsonProperty;
+ return ((JsonProperty)property).getName();
+ }
+
+ @NotNull
+ @Override
+ public PsiElement createProperty(@NotNull String name, @NotNull String value) {
+ return myGenerator.createProperty(name, value);
+ }
+
+ @Override
+ public boolean ensureComma(PsiElement backward, PsiElement self, PsiElement newElement) {
+ if (backward instanceof JsonProperty) {
+ self.addAfter(myGenerator.createComma(), backward);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void removeIfComma(PsiElement forward) {
+ if (forward instanceof LeafPsiElement && ((LeafPsiElement)forward).getElementType() == JsonElementTypes.COMMA) {
+ forward.delete();
+ }
+ }
+
+ @Override
+ public boolean fixWhitespaceBefore(PsiElement initialElement, PsiElement element) {
+ return true;
+ }
+ };
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonPointerReferenceProvider.java b/json/src/com/jetbrains/jsonSchema/impl/JsonPointerReferenceProvider.java
new file mode 100644
index 00000000..e0a9ed84
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonPointerReferenceProvider.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2000-2017 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.codeInsight.completion.CompletionUtil;
+import com.intellij.codeInsight.completion.CompletionUtilCore;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.icons.AllIcons;
+import com.intellij.json.JsonFileType;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.paths.WebReference;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.PsiReferenceProvider;
+import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileInfoManager;
+import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference;
+import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceSet;
+import com.intellij.util.ArrayUtil;
+import com.intellij.util.ProcessingContext;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.JsonPointerResolver;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.List;
+
+import static com.jetbrains.jsonSchema.JsonPointerUtil.*;
+import static com.jetbrains.jsonSchema.remote.JsonFileResolver.isHttpPath;
+
+/**
+ * @author Irina.Chernushina on 3/31/2016.
+ */
+public class JsonPointerReferenceProvider extends PsiReferenceProvider {
+ private final boolean myOnlyFilePart;
+
+ public JsonPointerReferenceProvider(boolean onlyFilePart) {
+ myOnlyFilePart = onlyFilePart;
+ }
+
+ @NotNull
+ @Override
+ public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
+ if (!(element instanceof JsonStringLiteral)) return PsiReference.EMPTY_ARRAY;
+ List<PsiReference> refs = ContainerUtil.newArrayList();
+
+ List<Pair<TextRange, String>> fragments = ((JsonStringLiteral)element).getTextFragments();
+ if (fragments.size() != 1) return PsiReference.EMPTY_ARRAY;
+ Pair<TextRange, String> fragment = fragments.get(0);
+ String originalText = element.getText();
+ int hash = originalText.indexOf('#');
+ final JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter splitter = new JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter(fragment.second);
+ String id = splitter.getSchemaId();
+ if (id != null) {
+ if (id.startsWith("#")) {
+ refs.add(new JsonSchemaIdReference((JsonValue)element, id));
+ }
+ else {
+ addFileOrWebReferences(element, refs, hash, id);
+ }
+ }
+ if (!myOnlyFilePart) {
+ String relativePath = normalizeSlashes(JsonSchemaService.normalizeId(splitter.getRelativePath()));
+ List<String> parts1 = split(relativePath);
+ String[] strings = ContainerUtil.toArray(parts1, String[]::new);
+ List<String> parts2 = split(normalizeSlashes(originalText.substring(hash + 1)));
+ if (strings.length == parts2.size()) {
+ int start = hash + 2;
+ for (int i = 0; i < parts2.size(); i++) {
+ int length = parts2.get(i).length();
+ if (i == parts2.size() - 1) length--;
+ refs.add(new JsonPointerReference((JsonValue)element, new TextRange(start, start + length),
+ (id == null ? "" : id) + "#/" + StringUtil.join(strings, 0, i + 1, "/")));
+ start += length + 1;
+ }
+ }
+ }
+ return refs.size() == 0 ? PsiReference.EMPTY_ARRAY : ContainerUtil.toArray(refs, PsiReference[]::new);
+ }
+
+ private void addFileOrWebReferences(@NotNull PsiElement element, List<PsiReference> refs, int hashIndex, String id) {
+ if (isHttpPath(id)) {
+ refs.add(new WebReference(element, new TextRange(1, hashIndex >= 0 ? hashIndex : id.length() + 1), id));
+ return;
+ }
+
+ ContainerUtil.addAll(refs, new FileReferenceSet(id, element, 1, null, true,
+ true, new JsonFileType[]{JsonFileType.INSTANCE}) {
+ @Override
+ public boolean isEmptyPathAllowed() {
+ return true;
+ }
+
+ @Override
+ protected boolean isSoft() {
+ return true;
+ }
+
+ @Override
+ public FileReference createFileReference(TextRange range, int index, String text) {
+ if (hashIndex != -1 && range.getStartOffset() >= hashIndex) return null;
+ if (hashIndex != -1 && range.getEndOffset() > hashIndex) {
+ range = new TextRange(range.getStartOffset(), hashIndex);
+ text = text.substring(0, text.indexOf('#'));
+ }
+ return new FileReference(this, range, index, text) {
+ @Override
+ protected Object createLookupItem(PsiElement candidate) {
+ return FileInfoManager.getFileLookupItem(candidate);
+ }
+ };
+ }
+ }.getAllReferences());
+ }
+
+ @Nullable
+ static PsiElement resolveForPath(PsiElement element, String text, boolean alwaysRoot) {
+ final JsonSchemaService service = JsonSchemaService.Impl.get(element.getProject());
+ final JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter splitter = new JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter(text);
+ VirtualFile schemaFile = CompletionUtil.getOriginalOrSelf(element.getContainingFile()).getVirtualFile();
+ if (splitter.isAbsolute()) {
+ assert splitter.getSchemaId() != null;
+ schemaFile = service.findSchemaFileByReference(splitter.getSchemaId(), schemaFile);
+ if (schemaFile == null) return null;
+ }
+
+ final String normalized = JsonSchemaService.normalizeId(splitter.getRelativePath());
+ if (!alwaysRoot && (StringUtil.isEmptyOrSpaces(normalized) || split(normalizeSlashes(normalized)).size() == 0)) {
+ return element.getManager().findFile(schemaFile);
+ }
+ final List<String> chain = split(normalizeSlashes(normalized));
+ final JsonSchemaObject schemaObject = service.getSchemaObjectForSchemaFile(schemaFile);
+ if (schemaObject == null) return null;
+
+ return new JsonPointerResolver(schemaObject.getJsonObject(), StringUtil.join(chain, "/")).resolve();
+ }
+
+ public static class JsonSchemaIdReference extends JsonSchemaBaseReference<JsonValue> {
+ private final String myText;
+
+ private JsonSchemaIdReference(JsonValue element, String text) {
+ super(element, getRange(element));
+ myText = text;
+ }
+
+ @NotNull
+ private static TextRange getRange(JsonValue element) {
+ final TextRange range = element.getTextRange().shiftLeft(element.getTextOffset());
+ return new TextRange(range.getStartOffset() + 1, range.getEndOffset() - 1);
+ }
+
+ @Nullable
+ @Override
+ public PsiElement resolveInner() {
+ final String id = JsonCachedValues.resolveId(myElement.getContainingFile(), myText);
+ if (id == null) return null;
+ return resolveForPath(myElement, "#" + id, false);
+ }
+
+ @NotNull
+ @Override
+ public Object[] getVariants() {
+ return JsonCachedValues.getAllIdsInFile(myElement.getContainingFile()).toArray();
+ }
+ }
+
+ private static class JsonPointerReference extends JsonSchemaBaseReference<JsonValue> {
+ private final String myFullPath;
+
+ JsonPointerReference(JsonValue element, TextRange textRange, String curPath) {
+ super(element, textRange);
+ myFullPath = curPath;
+ }
+
+ @NotNull
+ @Override
+ public String getCanonicalText() {
+ return myFullPath;
+ }
+
+ @Nullable
+ @Override
+ public PsiElement resolveInner() {
+ return resolveForPath(myElement, getCanonicalText(), false);
+ }
+
+ @Override
+ protected boolean isIdenticalTo(JsonSchemaBaseReference that) {
+ return super.isIdenticalTo(that) && getRangeInElement().equals(that.getRangeInElement());
+ }
+
+ @NotNull
+ @Override
+ public Object[] getVariants() {
+ String text = getCanonicalText();
+ int index = text.indexOf(CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED);
+ if (index >= 0) {
+ String part = text.substring(0, index);
+ text = prepare(part);
+ String prefix = null;
+ PsiElement element = resolveForPath(myElement, text, true);
+ int indexOfSlash = part.lastIndexOf('/');
+ if (indexOfSlash != -1 && indexOfSlash < text.length() - 1 && indexOfSlash < index) {
+ prefix = text.substring(indexOfSlash + 1);
+ element = resolveForPath(myElement, prepare(text.substring(0, indexOfSlash)), true);
+ }
+ String finalPrefix = prefix;
+ if (element instanceof JsonObject) {
+ return ((JsonObject)element).getPropertyList().stream()
+ .filter(p -> p.getValue() instanceof JsonContainer && (finalPrefix == null || p.getName().startsWith(finalPrefix)))
+ .map(p -> LookupElementBuilder.create(p, escapeForJsonPointer(p.getName()))
+ .withIcon(getIcon(p.getValue()))).toArray();
+ }
+ else if (element instanceof JsonArray) {
+ List<JsonValue> list = ((JsonArray)element).getValueList();
+ List<Object> values = ContainerUtil.newLinkedList();
+ for (int i = 0; i < list.size(); i++) {
+ String stringValue = String.valueOf(i);
+ if (prefix != null && !stringValue.startsWith(prefix)) continue;
+ values.add(LookupElementBuilder.create(stringValue).withIcon(getIcon(list.get(i))));
+ }
+ return ContainerUtil.toArray(values, Object[]::new);
+ }
+ }
+
+ return ArrayUtil.EMPTY_OBJECT_ARRAY;
+ }
+
+ private static Icon getIcon(JsonValue value) {
+ if (value instanceof JsonObject) {
+ return AllIcons.Json.Object;
+ }
+ else if (value instanceof JsonArray) {
+ return AllIcons.Json.Array;
+ }
+ return AllIcons.Nodes.Property;
+ }
+ }
+
+ @NotNull
+ private static String prepare(String part) {
+ return part.endsWith("#/") ? part : StringUtil.trimEnd(part, '/');
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonRequiredPropsReferenceProvider.java b/json/src/com/jetbrains/jsonSchema/impl/JsonRequiredPropsReferenceProvider.java
new file mode 100644
index 00000000..44cd3704
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonRequiredPropsReferenceProvider.java
@@ -0,0 +1,62 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.psi.ElementManipulators;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.PsiReferenceProvider;
+import com.intellij.util.ProcessingContext;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Optional;
+
+public class JsonRequiredPropsReferenceProvider extends PsiReferenceProvider {
+ @NotNull
+ @Override
+ public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
+ return new PsiReference[] {new JsonRequiredPropReference((JsonStringLiteral)element)};
+ }
+
+ @Nullable
+ public static JsonObject findPropertiesObject(PsiElement element) {
+ PsiElement parent = getParentSafe(getParentSafe(getParentSafe(element)));
+ if (!(parent instanceof JsonObject)) return null;
+ Optional<JsonProperty> propertiesProp =
+ ((JsonObject)parent).getPropertyList().stream().filter(p -> "properties".equals(p.getName())).findFirst();
+ if (propertiesProp.isPresent()) {
+ JsonValue value = propertiesProp.get().getValue();
+ if (value instanceof JsonObject) {
+ return (JsonObject)value;
+ }
+ }
+ return null;
+ }
+
+ private static PsiElement getParentSafe(@Nullable PsiElement element) {
+ return element == null ? null : element.getParent();
+ }
+
+ private static class JsonRequiredPropReference extends JsonSchemaBaseReference<JsonStringLiteral> {
+ JsonRequiredPropReference(JsonStringLiteral element) {
+ super(element, ElementManipulators.getValueTextRange(element));
+ }
+
+ @Nullable
+ @Override
+ public PsiElement resolveInner() {
+ JsonObject propertiesObject = findPropertiesObject(getElement());
+ if (propertiesObject != null) {
+ String name = getElement().getValue();
+ for (JsonProperty property : propertiesObject.getPropertyList()) {
+ if (name.equals(property.getName())) return property;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java
new file mode 100644
index 00000000..1fb1f5a2
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java
@@ -0,0 +1,1147 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.JsonBundle;
+import com.intellij.json.psi.JsonContainer;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+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.PsiElement;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.SmartList;
+import com.intellij.util.ThreeState;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.containers.MultiMap;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.extension.adapters.JsonArrayValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 4/25/2017.
+ */
+class JsonSchemaAnnotatorChecker {
+ private static final Set<JsonSchemaType> PRIMITIVE_TYPES =
+ ContainerUtil.set(JsonSchemaType._integer, JsonSchemaType._number, JsonSchemaType._boolean, JsonSchemaType._string, JsonSchemaType._null);
+ private final Map<PsiElement, JsonValidationError> myErrors;
+ private final JsonComplianceCheckerOptions myOptions;
+ private boolean myHadTypeError;
+ private static final String ENUM_MISMATCH_PREFIX = "Value should be one of: ";
+
+ protected JsonSchemaAnnotatorChecker(JsonComplianceCheckerOptions options) {
+ myOptions = options;
+ myErrors = new HashMap<>();
+ }
+
+ public Map<PsiElement, JsonValidationError> getErrors() {
+ return myErrors;
+ }
+
+ public boolean isHadTypeError() {
+ return myHadTypeError;
+ }
+
+ public static JsonSchemaAnnotatorChecker checkByMatchResult(@NotNull JsonValueAdapter elementToCheck,
+ @NotNull final MatchResult result,
+ @NotNull JsonComplianceCheckerOptions options) {
+ final List<JsonSchemaAnnotatorChecker> checkers = new ArrayList<>();
+ if (result.myExcludingSchemas.isEmpty() && result.mySchemas.size() == 1) {
+ final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(options);
+ checker.checkByScheme(elementToCheck, result.mySchemas.iterator().next());
+ checkers.add(checker);
+ }
+ else {
+ if (!result.mySchemas.isEmpty()) {
+ checkers.add(processSchemasVariants(result.mySchemas, elementToCheck, false, options).getSecond());
+ }
+ if (!result.myExcludingSchemas.isEmpty()) {
+ // we can have several oneOf groups, each about, for instance, a part of properties
+ // - then we should allow properties from neighbour schemas (even if additionalProperties=false)
+ final List<JsonSchemaAnnotatorChecker> list = result.myExcludingSchemas.stream()
+ .map(group -> processSchemasVariants(group, elementToCheck, true, options).getSecond()).collect(Collectors.toList());
+ checkers.add(mergeErrors(list, options, result.myExcludingSchemas));
+ }
+ }
+ if (checkers.isEmpty()) return null;
+ if (checkers.size() == 1) return checkers.get(0);
+
+ return checkers.stream()
+ .filter(checker -> !checker.isHadTypeError())
+ .findFirst()
+ .orElse(checkers.get(0));
+ }
+
+ private static JsonSchemaAnnotatorChecker mergeErrors(@NotNull List<JsonSchemaAnnotatorChecker> list,
+ @NotNull JsonComplianceCheckerOptions options,
+ List<Collection<? extends JsonSchemaObject>> excludingSchemas) {
+ final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(options);
+
+ for (JsonSchemaAnnotatorChecker ch: list) {
+ for (Map.Entry<PsiElement, JsonValidationError> element: ch.myErrors.entrySet()) {
+ JsonValidationError error = element.getValue();
+ if (error.getFixableIssueKind() == JsonValidationError.FixableIssueKind.ProhibitedProperty) {
+ String propertyName = ((JsonValidationError.ProhibitedPropertyIssueData)error.getIssueData()).propertyName;
+ boolean skip = false;
+ for (Collection<? extends JsonSchemaObject> objects : excludingSchemas) {
+ Set<String> keys = objects.stream().map(o -> o.getProperties().keySet()).flatMap(Set::stream).collect(Collectors.toSet());
+ if (keys.contains(propertyName)) skip = true;
+ }
+ if (skip) continue;
+ }
+ checker.myErrors.put(element.getKey(), error);
+ }
+ }
+ return checker;
+ }
+
+ private void error(final String error, final PsiElement holder,
+ JsonErrorPriority priority) {
+ error(error, holder, JsonValidationError.FixableIssueKind.None, null, priority);
+ }
+
+ private void error(final PsiElement newHolder, JsonValidationError error) {
+ error(error.getMessage(), newHolder, error.getFixableIssueKind(), error.getIssueData(), error.getPriority());
+ }
+
+ private void error(final String error, final PsiElement holder,
+ JsonValidationError.FixableIssueKind fixableIssueKind,
+ JsonValidationError.IssueData data,
+ JsonErrorPriority priority) {
+ if (myErrors.containsKey(holder)) return;
+ myErrors.put(holder, new JsonValidationError(error, fixableIssueKind, data, priority));
+ }
+
+ private void typeError(final @NotNull PsiElement value, final @NotNull JsonSchemaType... allowedTypes) {
+ if (allowedTypes.length > 0) {
+ if (allowedTypes.length == 1) {
+ error(String.format("Type is not allowed. Expected: %s.", allowedTypes[0].getName()), value,
+ JsonValidationError.FixableIssueKind.ProhibitedType,
+ new JsonValidationError.TypeMismatchIssueData(allowedTypes),
+ JsonErrorPriority.TYPE_MISMATCH);
+ } else {
+ final String typesText = Arrays.stream(allowedTypes)
+ .map(JsonSchemaType::getName)
+ .distinct()
+ .sorted(Comparator.naturalOrder())
+ .collect(Collectors.joining(", "));
+ error(String.format("Type is not allowed. Expected one of: %s.", typesText), value,
+ JsonValidationError.FixableIssueKind.ProhibitedType,
+ new JsonValidationError.TypeMismatchIssueData(allowedTypes),
+ JsonErrorPriority.TYPE_MISMATCH);
+ }
+ } else {
+ error("Type is not allowed", value, JsonErrorPriority.TYPE_MISMATCH);
+ }
+ myHadTypeError = true;
+ }
+
+ public void checkByScheme(@NotNull JsonValueAdapter value, @NotNull JsonSchemaObject schema) {
+ final JsonSchemaType type = JsonSchemaType.getType(value);
+ checkForEnum(value.getDelegate(), schema);
+ boolean checkedNumber = false;
+ boolean checkedString = false;
+ boolean checkedArray = false;
+ boolean checkedObject = false;
+ if (type != null) {
+ JsonSchemaType schemaType = getMatchingSchemaType(schema, type);
+ if (schemaType != null && !schemaType.equals(type)) {
+ typeError(value.getDelegate(), schemaType);
+ }
+ else {
+ if (JsonSchemaType._string_number.equals(type)) {
+ checkNumber(value.getDelegate(), schema, type);
+ checkedNumber = true;
+ checkString(value.getDelegate(), schema);
+ checkedString = true;
+ }
+ else if (JsonSchemaType._number.equals(type) || JsonSchemaType._integer.equals(type)) {
+ checkNumber(value.getDelegate(), schema, type);
+ checkedNumber = true;
+ }
+ else if (JsonSchemaType._string.equals(type)) {
+ checkString(value.getDelegate(), schema);
+ checkedString = true;
+ }
+ else if (JsonSchemaType._array.equals(type)) {
+ checkArray(value, schema);
+ checkedArray = true;
+ }
+ else if (JsonSchemaType._object.equals(type)) {
+ checkObject(value, schema);
+ checkedObject = true;
+ }
+ }
+ }
+
+ if ((!myHadTypeError || myErrors.isEmpty()) && !value.isShouldBeIgnored()) {
+ PsiElement delegate = value.getDelegate();
+ if (!checkedNumber && schema.hasNumericChecks() && value.isNumberLiteral()) {
+ checkNumber(delegate, schema, JsonSchemaType._number);
+ }
+ if (!checkedString && schema.hasStringChecks() && value.isStringLiteral()) {
+ checkString(delegate, schema);
+ checkedString = true;
+ }
+ if (!checkedArray && schema.hasArrayChecks() && value.isArray()) {
+ checkArray(value, schema);
+ checkedArray = true;
+ }
+ if (hasMinMaxLengthChecks(schema)) {
+ if (value.isStringLiteral()) {
+ if (!checkedString) {
+ checkString(delegate, schema);
+ }
+ }
+ else if (value.isArray()) {
+ if (!checkedArray) {
+ checkArray(value, schema);
+ }
+ }
+ }
+ if (!checkedObject && schema.hasObjectChecks() && value.isObject()) {
+ checkObject(value, schema);
+ }
+ }
+
+ if (schema.getNot() != null) {
+ final MatchResult result = new JsonSchemaResolver(schema.getNot()).detailedResolve();
+ if (result.mySchemas.isEmpty() && result.myExcludingSchemas.isEmpty()) return;
+
+ // if 'not' uses reference to owning schema back -> do not check, seems it does not make any sense
+ if (result.mySchemas.stream().anyMatch(s -> schema.getJsonObject().equals(s.getJsonObject())) ||
+ result.myExcludingSchemas.stream().flatMap(Collection::stream)
+ .anyMatch(s -> schema.getJsonObject().equals(s.getJsonObject()))) return;
+
+ final JsonSchemaAnnotatorChecker checker = checkByMatchResult(value, result, myOptions);
+ if (checker == null || checker.isCorrect()) error("Validates against 'not' schema", value.getDelegate(), JsonErrorPriority.NOT_SCHEMA);
+ }
+
+ if (schema.getIf() != null) {
+ MatchResult result = new JsonSchemaResolver(schema.getIf()).detailedResolve();
+ if (result.mySchemas.isEmpty() && result.myExcludingSchemas.isEmpty()) return;
+
+ final JsonSchemaAnnotatorChecker checker = checkByMatchResult(value, result, myOptions);
+ if (checker != null) {
+ if (checker.isCorrect()) {
+ JsonSchemaObject then = schema.getThen();
+ if (then == null) {
+ error("Validates against 'if' branch but no 'then' branch is present", value.getDelegate(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ else {
+ checkObjectBySchemaRecordErrors(then, value);
+ }
+ }
+ else {
+ JsonSchemaObject schemaElse = schema.getElse();
+ if (schemaElse == null) {
+ error("Validates counter 'if' branch but no 'else' branch is present", value.getDelegate(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ else {
+ checkObjectBySchemaRecordErrors(schemaElse, value);
+ }
+ }
+ }
+ }
+ }
+
+ private void checkObjectBySchemaRecordErrors(@NotNull JsonSchemaObject schema, @NotNull JsonValueAdapter object) {
+ final JsonSchemaAnnotatorChecker checker = checkByMatchResult(object, new JsonSchemaResolver(schema).detailedResolve(), myOptions);
+ if (checker != null) {
+ myHadTypeError = checker.isHadTypeError();
+ myErrors.putAll(checker.getErrors());
+ }
+ }
+
+ private void checkObject(@NotNull JsonValueAdapter value, @NotNull JsonSchemaObject schema) {
+ final JsonObjectValueAdapter object = value.getAsObject();
+ if (object == null) return;
+
+ final List<JsonPropertyAdapter> propertyList = object.getPropertyList();
+ final Set<String> set = new HashSet<>();
+ for (JsonPropertyAdapter property : propertyList) {
+ final String name = StringUtil.notNullize(property.getName());
+ JsonSchemaObject propertyNamesSchema = schema.getPropertyNamesSchema();
+ if (propertyNamesSchema != null) {
+ JsonValueAdapter nameValueAdapter = property.getNameValueAdapter();
+ if (nameValueAdapter != null) {
+ checkByScheme(nameValueAdapter, propertyNamesSchema);
+ }
+ }
+
+ final JsonSchemaVariantsTreeBuilder.Step step = JsonSchemaVariantsTreeBuilder.Step.createPropertyStep(name);
+ final Pair<ThreeState, JsonSchemaObject> pair = step.step(schema, true);
+ if (ThreeState.NO.equals(pair.getFirst()) && !set.contains(name)) {
+ error(JsonBundle.message("json.schema.annotation.not.allowed.property", name), property.getDelegate(),
+ JsonValidationError.FixableIssueKind.ProhibitedProperty,
+ new JsonValidationError.ProhibitedPropertyIssueData(name), JsonErrorPriority.LOW_PRIORITY);
+ }
+ else if (ThreeState.UNSURE.equals(pair.getFirst())) {
+ for (JsonValueAdapter propertyValue : property.getValues()) {
+ checkObjectBySchemaRecordErrors(pair.getSecond(), propertyValue);
+ }
+ }
+ set.add(name);
+ }
+
+ if (object.shouldCheckIntegralRequirements()) {
+ final Set<String> required = schema.getRequired();
+ if (required != null) {
+ HashSet<String> requiredNames = ContainerUtil.newHashSet(required);
+ requiredNames.removeAll(set);
+ if (!requiredNames.isEmpty()) {
+ JsonValidationError.MissingMultiplePropsIssueData data = createMissingPropertiesData(schema, requiredNames);
+ error("Missing required " + data.getMessage(false), value.getDelegate(), JsonValidationError.FixableIssueKind.MissingProperty, data,
+ JsonErrorPriority.MISSING_PROPS);
+ }
+ }
+ if (schema.getMinProperties() != null && propertyList.size() < schema.getMinProperties()) {
+ error("Number of properties is less than " + schema.getMinProperties(), value.getDelegate(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ if (schema.getMaxProperties() != null && propertyList.size() > schema.getMaxProperties()) {
+ error("Number of properties is greater than " + schema.getMaxProperties(), value.getDelegate(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ final Map<String, List<String>> dependencies = schema.getPropertyDependencies();
+ if (dependencies != null) {
+ for (Map.Entry<String, List<String>> entry : dependencies.entrySet()) {
+ if (set.contains(entry.getKey())) {
+ final List<String> list = entry.getValue();
+ HashSet<String> deps = ContainerUtil.newHashSet(list);
+ deps.removeAll(set);
+ if (!deps.isEmpty()) {
+ JsonValidationError.MissingMultiplePropsIssueData data = createMissingPropertiesData(schema, deps);
+ error("Dependency is violated: " + data.getMessage(false) + " must be specified, since '" + entry.getKey() + "' is specified",
+ value.getDelegate(),
+ JsonValidationError.FixableIssueKind.MissingProperty,
+ data, JsonErrorPriority.MISSING_PROPS);
+ }
+ }
+ }
+ }
+ final Map<String, JsonSchemaObject> schemaDependencies = schema.getSchemaDependencies();
+ if (schemaDependencies != null) {
+ for (Map.Entry<String, JsonSchemaObject> entry : schemaDependencies.entrySet()) {
+ if (set.contains(entry.getKey())) {
+ checkObjectBySchemaRecordErrors(entry.getValue(), value);
+ }
+ }
+ }
+ }
+
+ validateAsJsonSchema(object.getDelegate());
+ }
+
+ @Nullable
+ private static Object getDefaultValueFromEnum(@NotNull JsonSchemaObject propertySchema, @NotNull Ref<Integer> enumCount) {
+ List<Object> enumValues = propertySchema.getEnum();
+ if (enumValues != null) {
+ enumCount.set(enumValues.size());
+ if (enumValues.size() == 1) {
+ Object defaultObject = enumValues.get(0);
+ return defaultObject instanceof String ? StringUtil.unquoteString((String)defaultObject) : defaultObject;
+ }
+ }
+ return null;
+ }
+
+ @NotNull
+ private static JsonValidationError.MissingMultiplePropsIssueData createMissingPropertiesData(@NotNull JsonSchemaObject schema,
+ HashSet<String> requiredNames) {
+ List<JsonValidationError.MissingPropertyIssueData> allProps = ContainerUtil.newArrayList();
+ for (String req: requiredNames) {
+ JsonSchemaObject propertySchema = resolvePropertySchema(schema, req);
+ Object defaultValue = propertySchema == null ? null : propertySchema.getDefault();
+ Ref<Integer> enumCount = Ref.create(0);
+
+ JsonSchemaType type = null;
+
+ if (propertySchema != null) {
+ MatchResult result = null;
+ Object valueFromEnum = getDefaultValueFromEnum(propertySchema, enumCount);
+ if (valueFromEnum != null) {
+ defaultValue = valueFromEnum;
+ }
+ else {
+ result = new JsonSchemaResolver(propertySchema).detailedResolve();
+ if (result.mySchemas.size() == 1) {
+ valueFromEnum = getDefaultValueFromEnum(result.mySchemas.get(0), enumCount);
+ if (valueFromEnum != null) {
+ defaultValue = valueFromEnum;
+ }
+ }
+ }
+ type = propertySchema.getType();
+ if (type == null) {
+ if (result == null) {
+ result = new JsonSchemaResolver(propertySchema).detailedResolve();
+ }
+ if (result.mySchemas.size() == 1) {
+ type = result.mySchemas.get(0).getType();
+ }
+ }
+ }
+ allProps.add(new JsonValidationError.MissingPropertyIssueData(req,
+ type,
+ defaultValue,
+ enumCount.get()));
+ }
+
+ return new JsonValidationError.MissingMultiplePropsIssueData(allProps);
+ }
+
+ private static JsonSchemaObject resolvePropertySchema(@NotNull JsonSchemaObject schema, String req) {
+ if (schema.getProperties().containsKey(req)) {
+ return schema.getProperties().get(req);
+ }
+ else {
+ JsonSchemaObject propertySchema = schema.getMatchingPatternPropertySchema(req);
+ if (propertySchema != null) {
+ return propertySchema;
+ }
+ else {
+ JsonSchemaObject additionalPropertiesSchema = schema.getAdditionalPropertiesSchema();
+ if (additionalPropertiesSchema != null) {
+ return additionalPropertiesSchema;
+ }
+ }
+ }
+ return null;
+ }
+
+ private void validateAsJsonSchema(@NotNull PsiElement objElement) {
+ final JsonObject object = ObjectUtils.tryCast(objElement, JsonObject.class);
+ if (object == null) return;
+
+ if (!JsonSchemaService.isSchemaFile(objElement.getContainingFile())) {
+ return;
+ }
+
+ final VirtualFile schemaFile = object.getContainingFile().getVirtualFile();
+ if (schemaFile == null) return;
+
+ final JsonSchemaObject schemaObject = JsonSchemaService.Impl.get(object.getProject()).getSchemaObjectForSchemaFile(schemaFile);
+ if (schemaObject == null) return;
+
+ final List<JsonSchemaVariantsTreeBuilder.Step> position = JsonOriginalPsiWalker.INSTANCE.findPosition(object, true);
+ if (position == null) return;
+ final List<JsonSchemaVariantsTreeBuilder.Step> steps = skipProperties(position);
+ // !! not root schema, because we validate the schema written in the file itself
+ final MatchResult result = new JsonSchemaResolver(schemaObject, false, steps).detailedResolve();
+ for (JsonSchemaObject s: result.mySchemas) {
+ reportInvalidPatternProperties(s);
+ reportPatternErrors(s);
+ }
+ result.myExcludingSchemas.stream().flatMap(Collection::stream).filter(s -> schemaFile.equals(s.getSchemaFile()))
+ .forEach(schema -> {
+ reportInvalidPatternProperties(schema);
+ reportPatternErrors(schema);
+ });
+ }
+
+ private void reportPatternErrors(JsonSchemaObject schema) {
+ for (JsonSchemaObject prop : schema.getProperties().values()) {
+ final String patternError = prop.getPatternError();
+ if (patternError == null || prop.getPattern() == null) {
+ continue;
+ }
+
+ final JsonContainer element = prop.getJsonObject();
+ if (!(element instanceof JsonObject) || !element.isValid()) {
+ continue;
+ }
+
+ final JsonProperty pattern = ((JsonObject)element).findProperty("pattern");
+ if (pattern != null) {
+ error(StringUtil.convertLineSeparators(patternError), pattern.getValue(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ }
+
+ private void reportInvalidPatternProperties(JsonSchemaObject schema) {
+ final Map<JsonContainer, String> invalidPatternProperties = schema.getInvalidPatternProperties();
+ if (invalidPatternProperties == null) return;
+
+ for (Map.Entry<JsonContainer, String> entry : invalidPatternProperties.entrySet()) {
+ final JsonContainer element = entry.getKey();
+ if (element == null || !element.isValid()) continue;
+ final PsiElement parent = element.getParent();
+ if (parent instanceof JsonProperty) {
+ error(StringUtil.convertLineSeparators(entry.getValue()), ((JsonProperty)parent).getNameElement(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ }
+
+ private static List<JsonSchemaVariantsTreeBuilder.Step> skipProperties(List<JsonSchemaVariantsTreeBuilder.Step> position) {
+ final Iterator<JsonSchemaVariantsTreeBuilder.Step> iterator = position.iterator();
+ boolean canSkip = true;
+ while (iterator.hasNext()) {
+ final JsonSchemaVariantsTreeBuilder.Step step = iterator.next();
+ if (canSkip && step.isFromObject() && JsonSchemaObject.PROPERTIES.equals(step.getName())) {
+ iterator.remove();
+ canSkip = false;
+ }
+ else {
+ canSkip = true;
+ }
+ }
+ return position;
+ }
+
+ private static boolean checkEnumValue(@NotNull Object object,
+ @NotNull JsonLikePsiWalker walker,
+ @Nullable JsonValueAdapter adapter,
+ @NotNull String text,
+ @NotNull BiFunction<String, String, Boolean> stringEq) {
+ if (object instanceof EnumArrayValueWrapper) {
+ if (adapter instanceof JsonArrayValueAdapter) {
+ List<JsonValueAdapter> elements = ((JsonArrayValueAdapter)adapter).getElements();
+ Object[] values = ((EnumArrayValueWrapper)object).getValues();
+ if (elements.size() == values.length) {
+ for (int i = 0; i < values.length; i++) {
+ if (!checkEnumValue(values[i], walker, elements.get(i), walker.getNodeTextForValidation(elements.get(i).getDelegate()), stringEq)) return false;
+ }
+ return true;
+ }
+ }
+ }
+ else if (object instanceof EnumObjectValueWrapper) {
+ if (adapter instanceof JsonObjectValueAdapter) {
+ List<JsonPropertyAdapter> props = ((JsonObjectValueAdapter)adapter).getPropertyList();
+ Map<String, Object> values = ((EnumObjectValueWrapper)object).getValues();
+ if (props.size() == values.size()) {
+ for (JsonPropertyAdapter prop : props) {
+ if (!values.containsKey(prop.getName())) return false;
+ for (JsonValueAdapter value : prop.getValues()) {
+ if (!checkEnumValue(values.get(prop.getName()), walker, value, walker.getNodeTextForValidation(value.getDelegate()), stringEq)) return false;
+ }
+ }
+
+ return true;
+ }
+ }
+ }
+ else {
+ if (walker.onlyDoubleQuotesForStringLiterals()) {
+ if (stringEq.apply(object.toString(), text)) return true;
+ }
+ else {
+ if (equalsIgnoreQuotes(object.toString(), text, walker.quotesForStringLiterals(), stringEq)) return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void checkForEnum(PsiElement value, JsonSchemaObject schema) {
+ List<Object> enumItems = schema.getEnum();
+ if (enumItems == null) return;
+ final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(value, schema);
+ if (walker == null) return;
+ final String text = StringUtil.notNullize(walker.getNodeTextForValidation(value));
+ BiFunction<String, String, Boolean> eq = myOptions.isCaseInsensitiveEnumCheck() ? String::equalsIgnoreCase : String::equals;
+ for (Object object : enumItems) {
+ if (checkEnumValue(object, walker, walker.createValueAdapter(value), text, eq)) return;
+ }
+ error(ENUM_MISMATCH_PREFIX + StringUtil.join(enumItems, o -> o.toString(), ", "), value,
+ JsonValidationError.FixableIssueKind.NonEnumValue, null, JsonErrorPriority.MEDIUM_PRIORITY);
+ }
+
+ private static boolean equalsIgnoreQuotes(@NotNull final String s1,
+ @NotNull final String s2,
+ boolean requireQuotedValues,
+ BiFunction<String, String, Boolean> eq) {
+ final boolean quoted1 = StringUtil.isQuotedString(s1);
+ final boolean quoted2 = StringUtil.isQuotedString(s2);
+ if (requireQuotedValues && quoted1 != quoted2) return false;
+ if (requireQuotedValues && !quoted1) return eq.apply(s1, s2);
+ return eq.apply(StringUtil.unquoteString(s1), StringUtil.unquoteString(s2));
+ }
+
+ private void checkArray(JsonValueAdapter value, JsonSchemaObject schema) {
+ final JsonArrayValueAdapter asArray = value.getAsArray();
+ if (asArray == null) return;
+ final List<JsonValueAdapter> elements = asArray.getElements();
+ if (schema.getMinLength() != null && elements.size() < schema.getMinLength()) {
+ error("Array is shorter than " + schema.getMinLength(), value.getDelegate(), JsonErrorPriority.LOW_PRIORITY);
+ return;
+ }
+ checkArrayItems(value, elements, schema);
+ }
+
+ @NotNull
+ private static Pair<JsonSchemaObject, JsonSchemaAnnotatorChecker> processSchemasVariants(
+ @NotNull final Collection<? extends JsonSchemaObject> collection,
+ @NotNull final JsonValueAdapter value, boolean isOneOf, JsonComplianceCheckerOptions options) {
+
+ final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(options);
+ final JsonSchemaType type = JsonSchemaType.getType(value);
+ JsonSchemaObject selected = null;
+ if (type == null) {
+ if (!value.isShouldBeIgnored()) checker.typeError(value.getDelegate(), getExpectedTypes(collection));
+ }
+ else {
+ final List<JsonSchemaObject> filtered = ContainerUtil.newArrayListWithCapacity(collection.size());
+ for (JsonSchemaObject schema: collection) {
+ if (!areSchemaTypesCompatible(schema, type)) continue;
+ filtered.add(schema);
+ }
+ if (filtered.isEmpty()) checker.typeError(value.getDelegate(), getExpectedTypes(collection));
+ else if (filtered.size() == 1) {
+ selected = filtered.get(0);
+ checker.checkByScheme(value, selected);
+ }
+ else {
+ if (isOneOf) {
+ selected = checker.processOneOf(value, filtered);
+ }
+ else {
+ selected = checker.processAnyOf(value, filtered);
+ }
+ }
+ }
+ return Pair.create(selected, checker);
+ }
+
+ private final static JsonSchemaType[] NO_TYPES = new JsonSchemaType[0];
+ private static JsonSchemaType[] getExpectedTypes(final Collection<? extends JsonSchemaObject> schemas) {
+ final List<JsonSchemaType> list = new ArrayList<>();
+ for (JsonSchemaObject schema : schemas) {
+ final JsonSchemaType type = schema.getType();
+ if (type != null) {
+ list.add(type);
+ } else {
+ final Set<JsonSchemaType> variants = schema.getTypeVariants();
+ if (variants != null) {
+ list.addAll(variants);
+ }
+ }
+ }
+ return list.isEmpty() ? NO_TYPES : list.toArray(NO_TYPES);
+ }
+
+ public static boolean areSchemaTypesCompatible(@NotNull final JsonSchemaObject schema, @NotNull final JsonSchemaType type) {
+ final JsonSchemaType matchingSchemaType = getMatchingSchemaType(schema, type);
+ if (matchingSchemaType != null) return matchingSchemaType.equals(type);
+ if (schema.getEnum() != null) {
+ return PRIMITIVE_TYPES.contains(type);
+ }
+ return true;
+ }
+
+ @Nullable
+ private static JsonSchemaType getMatchingSchemaType(@NotNull JsonSchemaObject schema, @NotNull JsonSchemaType input) {
+ if (schema.getType() != null) {
+ final JsonSchemaType matchType = schema.getType();
+ if (matchType != null) {
+ if (JsonSchemaType._integer.equals(input) && JsonSchemaType._number.equals(matchType)) {
+ return input;
+ }
+ if (JsonSchemaType._string_number.equals(input) && (JsonSchemaType._number.equals(matchType)
+ || JsonSchemaType._integer.equals(matchType)
+ || JsonSchemaType._string.equals(matchType))) {
+ return input;
+ }
+ return matchType;
+ }
+ }
+ if (schema.getTypeVariants() != null) {
+ Set<JsonSchemaType> matchTypes = schema.getTypeVariants();
+ if (matchTypes.contains(input)) {
+ return input;
+ }
+ if (JsonSchemaType._integer.equals(input) && matchTypes.contains(JsonSchemaType._number)) {
+ return input;
+ }
+ if (JsonSchemaType._string_number.equals(input) &&
+ (matchTypes.contains(JsonSchemaType._number)
+ || matchTypes.contains(JsonSchemaType._integer)
+ || matchTypes.contains(JsonSchemaType._string))) {
+ return input;
+ }
+ //nothing matches, lets return one of the list so that other heuristics does not match
+ return matchTypes.iterator().next();
+ }
+ if (!schema.getProperties().isEmpty() && JsonSchemaType._object.equals(input)) return JsonSchemaType._object;
+ return null;
+ }
+
+ private void checkArrayItems(@NotNull JsonValueAdapter array, @NotNull final List<JsonValueAdapter> list, final JsonSchemaObject schema) {
+ if (schema.isUniqueItems()) {
+ final MultiMap<String, JsonValueAdapter> valueTexts = new MultiMap<>();
+ final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(array.getDelegate(), schema);
+ assert walker != null;
+ for (JsonValueAdapter adapter : list) {
+ valueTexts.putValue(walker.getNodeTextForValidation(adapter.getDelegate()), adapter);
+ }
+
+ for (Map.Entry<String, Collection<JsonValueAdapter>> entry: valueTexts.entrySet()) {
+ if (entry.getValue().size() > 1) {
+ for (JsonValueAdapter item: entry.getValue()) {
+ error("Item is not unique", item.getDelegate(), JsonErrorPriority.TYPE_MISMATCH);
+ }
+ }
+ }
+ }
+ if (schema.getContainsSchema() != null) {
+ boolean match = false;
+ for (JsonValueAdapter item: list) {
+ final JsonSchemaAnnotatorChecker checker = checkByMatchResult(item, new JsonSchemaResolver(schema.getContainsSchema()).detailedResolve(), myOptions);
+ if (checker == null || checker.myErrors.size() == 0 && !checker.myHadTypeError) {
+ match = true;
+ break;
+ }
+ }
+ if (!match) {
+ error("No match for 'contains' rule", array.getDelegate(), JsonErrorPriority.MEDIUM_PRIORITY);
+ }
+ }
+ if (schema.getItemsSchema() != null) {
+ for (JsonValueAdapter item : list) {
+ checkObjectBySchemaRecordErrors(schema.getItemsSchema(), item);
+ }
+ }
+ else if (schema.getItemsSchemaList() != null) {
+ final Iterator<JsonSchemaObject> iterator = schema.getItemsSchemaList().iterator();
+ for (JsonValueAdapter arrayValue : list) {
+ if (iterator.hasNext()) {
+ checkObjectBySchemaRecordErrors(iterator.next(), arrayValue);
+ }
+ else {
+ if (!Boolean.TRUE.equals(schema.getAdditionalItemsAllowed())) {
+ error("Additional items are not allowed", arrayValue.getDelegate(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ else if (schema.getAdditionalItemsSchema() != null) {
+ checkObjectBySchemaRecordErrors(schema.getAdditionalItemsSchema(), arrayValue);
+ }
+ }
+ }
+ }
+ if (schema.getMinItems() != null && list.size() < schema.getMinItems()) {
+ error("Array is shorter than " + schema.getMinItems(), array.getDelegate(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ if (schema.getMaxItems() != null && list.size() > schema.getMaxItems()) {
+ error("Array is longer than " + schema.getMaxItems(), array.getDelegate(), JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+
+ private static boolean hasMinMaxLengthChecks(JsonSchemaObject schema) {
+ return schema.getMinLength() != null || schema.getMaxLength() != null;
+ }
+
+ private void checkString(PsiElement propValue, JsonSchemaObject schema) {
+ final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(propValue, schema);
+ assert walker != null;
+ final String value = StringUtil.unquoteString(walker.getNodeTextForValidation(propValue));
+ if (schema.getMinLength() != null) {
+ if (value.length() < schema.getMinLength()) {
+ error("String is shorter than " + schema.getMinLength(), propValue, JsonErrorPriority.LOW_PRIORITY);
+ return;
+ }
+ }
+ if (schema.getMaxLength() != null) {
+ if (value.length() > schema.getMaxLength()) {
+ error("String is longer than " + schema.getMaxLength(), propValue, JsonErrorPriority.LOW_PRIORITY);
+ return;
+ }
+ }
+ if (schema.getPattern() != null) {
+ if (schema.getPatternError() != null) {
+ error("Can not check string by pattern because of error: " + StringUtil.convertLineSeparators(schema.getPatternError()),
+ propValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ if (!schema.checkByPattern(value)) {
+ error("String is violating the pattern: '" + StringUtil.convertLineSeparators(schema.getPattern()) + "'", propValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ // I think we are not gonna to support format, there are a couple of RFCs there to check upon..
+ /*
+ if (schema.getFormat() != null) {
+ LOG.info("Unsupported property used: 'format'");
+ }*/
+ }
+
+ private void checkNumber(PsiElement propValue, JsonSchemaObject schema, JsonSchemaType schemaType) {
+ Number value;
+ final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(propValue, schema);
+ assert walker != null;
+ String valueText = walker.getNodeTextForValidation(propValue);
+ if (JsonSchemaType._integer.equals(schemaType)) {
+ try {
+ value = Integer.valueOf(valueText);
+ }
+ catch (NumberFormatException e) {
+ error("Integer value expected", propValue,
+ JsonValidationError.FixableIssueKind.TypeMismatch,
+ new JsonValidationError.TypeMismatchIssueData(new JsonSchemaType[]{schemaType}), JsonErrorPriority.TYPE_MISMATCH);
+ return;
+ }
+ }
+ else {
+ try {
+ value = Double.valueOf(valueText);
+ }
+ catch (NumberFormatException e) {
+ if (!JsonSchemaType._string_number.equals(schemaType)) {
+ error("Double value expected", propValue,
+ JsonValidationError.FixableIssueKind.TypeMismatch,
+ new JsonValidationError.TypeMismatchIssueData(new JsonSchemaType[]{schemaType}), JsonErrorPriority.TYPE_MISMATCH);
+ }
+ return;
+ }
+ }
+ final Number multipleOf = schema.getMultipleOf();
+ if (multipleOf != null) {
+ final double leftOver = value.doubleValue() % multipleOf.doubleValue();
+ if (leftOver > 0.000001) {
+ final String multipleOfValue = String.valueOf(Math.abs(multipleOf.doubleValue() - multipleOf.intValue()) < 0.000001 ?
+ multipleOf.intValue() : multipleOf);
+ error("Is not multiple of " + multipleOfValue, propValue, JsonErrorPriority.LOW_PRIORITY);
+ return;
+ }
+ }
+
+ checkMinimum(schema, value, propValue, schemaType);
+ checkMaximum(schema, value, propValue, schemaType);
+ }
+
+ private void checkMaximum(JsonSchemaObject schema, Number value, PsiElement propertyValue,
+ @NotNull JsonSchemaType propValueType) {
+
+ Number exclusiveMaximumNumber = schema.getExclusiveMaximumNumber();
+ if (exclusiveMaximumNumber != null) {
+ if (JsonSchemaType._integer.equals(propValueType)) {
+ final int intValue = exclusiveMaximumNumber.intValue();
+ if (value.intValue() >= intValue) {
+ error("Greater than an exclusive maximum " + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ else {
+ final double doubleValue = exclusiveMaximumNumber.doubleValue();
+ if (value.doubleValue() >= doubleValue) {
+ error("Greater than an exclusive maximum " + exclusiveMaximumNumber, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ }
+ Number maximum = schema.getMaximum();
+ if (maximum == null) return;
+ boolean isExclusive = Boolean.TRUE.equals(schema.isExclusiveMaximum());
+ if (JsonSchemaType._integer.equals(propValueType)) {
+ final int intValue = maximum.intValue();
+ if (isExclusive) {
+ if (value.intValue() >= intValue) {
+ error("Greater than an exclusive maximum " + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ else {
+ if (value.intValue() > intValue) {
+ error("Greater than a maximum " + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ }
+ else {
+ final double doubleValue = maximum.doubleValue();
+ if (isExclusive) {
+ if (value.doubleValue() >= doubleValue) {
+ error("Greater than an exclusive maximum " + maximum, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ else {
+ if (value.doubleValue() > doubleValue) {
+ error("Greater than a maximum " + maximum, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ }
+ }
+
+ private void checkMinimum(JsonSchemaObject schema, Number value, PsiElement propertyValue,
+ @NotNull JsonSchemaType schemaType) {
+ // schema v6 - exclusiveMinimum is numeric now
+ Number exclusiveMinimumNumber = schema.getExclusiveMinimumNumber();
+ if (exclusiveMinimumNumber != null) {
+ if (JsonSchemaType._integer.equals(schemaType)) {
+ final int intValue = exclusiveMinimumNumber.intValue();
+ if (value.intValue() <= intValue) {
+ error("Less than an exclusive minimum" + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ else {
+ final double doubleValue = exclusiveMinimumNumber.doubleValue();
+ if (value.doubleValue() <= doubleValue) {
+ error("Less than an exclusive minimum " + exclusiveMinimumNumber, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ }
+
+ Number minimum = schema.getMinimum();
+ if (minimum == null) return;
+ boolean isExclusive = Boolean.TRUE.equals(schema.isExclusiveMinimum());
+ if (JsonSchemaType._integer.equals(schemaType)) {
+ final int intValue = minimum.intValue();
+ if (isExclusive) {
+ if (value.intValue() <= intValue) {
+ error("Less than an exclusive minimum " + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ else {
+ if (value.intValue() < intValue) {
+ error("Less than a minimum " + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ }
+ else {
+ final double doubleValue = minimum.doubleValue();
+ if (isExclusive) {
+ if (value.doubleValue() <= doubleValue) {
+ error("Less than an exclusive minimum " + minimum, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ else {
+ if (value.doubleValue() < doubleValue) {
+ error("Less than a minimum " + minimum, propertyValue, JsonErrorPriority.LOW_PRIORITY);
+ }
+ }
+ }
+ }
+
+ // returns the schema, selected for annotation
+ private JsonSchemaObject processOneOf(@NotNull JsonValueAdapter value, List<JsonSchemaObject> oneOf) {
+ final List<JsonSchemaAnnotatorChecker> candidateErroneousCheckers = ContainerUtil.newArrayList();
+ final List<JsonSchemaObject> candidateErroneousSchemas = ContainerUtil.newArrayList();
+ final List<JsonSchemaObject> correct = new SmartList<>();
+ for (JsonSchemaObject object : oneOf) {
+ // skip it if something JS awaited, we do not process it currently
+ if (object.isShouldValidateAgainstJSType()) continue;
+
+ final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(myOptions);
+ checker.checkByScheme(value, object);
+
+ if (checker.isCorrect()) {
+ candidateErroneousCheckers.clear();
+ candidateErroneousSchemas.clear();
+ correct.add(object);
+ }
+ else {
+ candidateErroneousCheckers.add(checker);
+ candidateErroneousSchemas.add(object);
+ }
+ }
+ if (correct.size() == 1) return correct.get(0);
+ if (correct.size() > 0) {
+ final JsonSchemaType type = JsonSchemaType.getType(value);
+ if (type != null) {
+ // also check maybe some currently not checked properties like format are different with schemes
+ // todo note that JsonSchemaObject#equals is broken by design, so normally it shouldn't be used until rewritten
+ // but for now we use it here to avoid similar schemas being marked as duplicates
+ if (ContainerUtil.newHashSet(correct).size() > 1 && !schemesDifferWithNotCheckedProperties(correct)) {
+ error("Validates to more than one variant", value.getDelegate(), JsonErrorPriority.MEDIUM_PRIORITY);
+ }
+ }
+ return ContainerUtil.getLastItem(correct);
+ }
+
+ return showErrorsAndGetLeastErroneous(candidateErroneousCheckers, candidateErroneousSchemas, true);
+ }
+
+ private static boolean schemesDifferWithNotCheckedProperties(@NotNull final List<JsonSchemaObject> list) {
+ return list.stream().anyMatch(s -> !StringUtil.isEmptyOrSpaces(s.getFormat()));
+ }
+
+ private enum AverageFailureAmount {
+ Light,
+ MissingItems,
+ Medium,
+ Hard,
+ NotSchema
+ }
+
+ @NotNull
+ private static AverageFailureAmount getAverageFailureAmount(@NotNull JsonSchemaAnnotatorChecker checker) {
+ int lowPriorityCount = 0;
+ boolean hasMedium = false;
+ boolean hasMissing = false;
+ boolean hasHard = false;
+ Collection<JsonValidationError> values = checker.getErrors().values();
+ for (JsonValidationError value: values) {
+ switch (value.getPriority()) {
+ case LOW_PRIORITY:
+ lowPriorityCount++;
+ break;
+ case MISSING_PROPS:
+ hasMissing = true;
+ break;
+ case MEDIUM_PRIORITY:
+ hasMedium = true;
+ break;
+ case TYPE_MISMATCH:
+ hasHard = true;
+ break;
+ case NOT_SCHEMA:
+ return AverageFailureAmount.NotSchema;
+ }
+ }
+
+ if (hasHard) {
+ return AverageFailureAmount.Hard;
+ }
+
+ // missing props should win against other conditions
+ if (hasMissing) {
+ return AverageFailureAmount.MissingItems;
+ }
+
+ if (hasMedium) {
+ return AverageFailureAmount.Medium;
+ }
+
+ return lowPriorityCount <= 3 ? AverageFailureAmount.Light : AverageFailureAmount.Medium;
+ }
+
+ // returns the schema, selected for annotation
+ private JsonSchemaObject processAnyOf(@NotNull JsonValueAdapter value, List<JsonSchemaObject> anyOf) {
+ final List<JsonSchemaAnnotatorChecker> candidateErroneousCheckers = ContainerUtil.newArrayList();
+ final List<JsonSchemaObject> candidateErroneousSchemas = ContainerUtil.newArrayList();
+
+ for (JsonSchemaObject object : anyOf) {
+ final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(myOptions);
+ checker.checkByScheme(value, object);
+ if (checker.isCorrect()) {
+ return object;
+ }
+ // maybe we still find the correct schema - continue to iterate
+ candidateErroneousCheckers.add(checker);
+ candidateErroneousSchemas.add(object);
+ }
+
+ return showErrorsAndGetLeastErroneous(candidateErroneousCheckers, candidateErroneousSchemas, false);
+ }
+
+ /**
+ * Filters schema validation results to get the result with the "minimal" amount of errors.
+ * This is needed in case of oneOf or anyOf conditions, when there exist no match.
+ * I.e., when we have multiple schema candidates, but none is applicable.
+ * In this case we need to show the most "suitable" error messages
+ * - by detecting the most "likely" schema corresponding to the current entity
+ */
+ @Nullable
+ private JsonSchemaObject showErrorsAndGetLeastErroneous(@NotNull List<JsonSchemaAnnotatorChecker> candidateErroneousCheckers,
+ @NotNull List<JsonSchemaObject> candidateErroneousSchemas,
+ boolean isOneOf) {
+ JsonSchemaObject current = null;
+ JsonSchemaObject currentWithMinAverage = null;
+ Optional<AverageFailureAmount> minAverage = candidateErroneousCheckers.stream()
+ .map(c -> getAverageFailureAmount(c))
+ .min(Comparator.comparingInt(c -> c.ordinal()));
+ int min = minAverage.orElse(AverageFailureAmount.Hard).ordinal();
+
+ int minErrorCount = candidateErroneousCheckers.stream().map(c -> c.getErrors().size()).min(Integer::compareTo).orElse(Integer.MAX_VALUE);
+
+ MultiMap<PsiElement, JsonValidationError> errorsWithMinAverage = MultiMap.create();
+ MultiMap<PsiElement, JsonValidationError> allErrors = MultiMap.create();
+ for (int i = 0; i < candidateErroneousCheckers.size(); i++) {
+ JsonSchemaAnnotatorChecker checker = candidateErroneousCheckers.get(i);
+ final boolean isMoreThanMinErrors = checker.getErrors().size() > minErrorCount;
+ final boolean isMoreThanAverage = getAverageFailureAmount(checker).ordinal() > min;
+ if (!isMoreThanMinErrors) {
+ if (isMoreThanAverage) {
+ currentWithMinAverage = candidateErroneousSchemas.get(i);
+ }
+ else {
+ current = candidateErroneousSchemas.get(i);
+ }
+
+ for (Map.Entry<PsiElement, JsonValidationError> entry: checker.getErrors().entrySet()) {
+ (isMoreThanAverage ? errorsWithMinAverage : allErrors).putValue(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ if (allErrors.isEmpty()) allErrors = errorsWithMinAverage;
+
+ for (Map.Entry<PsiElement, Collection<JsonValidationError>> entry : allErrors.entrySet()) {
+ Collection<JsonValidationError> value = entry.getValue();
+ if (value.size() == 0) continue;
+ if (value.size() == 1) {
+ error(entry.getKey(), value.iterator().next());
+ continue;
+ }
+ JsonValidationError error = tryMergeErrors(value, isOneOf);
+ if (error != null) {
+ error(entry.getKey(), error);
+ }
+ else {
+ for (JsonValidationError validationError : value) {
+ error(entry.getKey(), validationError);
+ }
+ }
+ }
+
+ if (current == null) {
+ current = currentWithMinAverage;
+ }
+ if (current == null) {
+ current = ContainerUtil.getLastItem(candidateErroneousSchemas);
+ }
+
+ return current;
+ }
+
+ @Nullable
+ private static JsonValidationError tryMergeErrors(@NotNull Collection<JsonValidationError> errors, boolean isOneOf) {
+ JsonValidationError.FixableIssueKind commonIssueKind = null;
+ for (JsonValidationError error : errors) {
+ JsonValidationError.FixableIssueKind currentIssueKind = error.getFixableIssueKind();
+ if (currentIssueKind == JsonValidationError.FixableIssueKind.None) return null;
+ else if (commonIssueKind == null) commonIssueKind = currentIssueKind;
+ else if (currentIssueKind != commonIssueKind) return null;
+ }
+
+ if (commonIssueKind == JsonValidationError.FixableIssueKind.NonEnumValue) {
+ return new JsonValidationError(ENUM_MISMATCH_PREFIX
+ + errors
+ .stream()
+ .map(e -> StringUtil.trimStart(e.getMessage(), ENUM_MISMATCH_PREFIX))
+ .map(e -> StringUtil.split(e, ", "))
+ .flatMap(e -> e.stream())
+ .distinct()
+ .collect(Collectors.joining(", ")), commonIssueKind, null, errors.iterator().next().getPriority());
+ }
+
+ if (commonIssueKind == JsonValidationError.FixableIssueKind.MissingProperty) {
+ String prefix = isOneOf ? "One of the following property sets is required: " : "Should have at least one of the following property sets: ";
+ return new JsonValidationError(prefix +
+ errors.stream().map(e -> (JsonValidationError.MissingMultiplePropsIssueData)e.getIssueData())
+ .map(d -> d.getMessage(false)).collect(Collectors.joining(", or ")),
+ isOneOf ? JsonValidationError.FixableIssueKind.MissingOneOfProperty : JsonValidationError.FixableIssueKind.MissingAnyOfProperty,
+ new JsonValidationError.MissingOneOfPropsIssueData(errors.stream().map(e -> (JsonValidationError.MissingMultiplePropsIssueData)e.getIssueData()).collect(
+ Collectors.toList())), errors.iterator().next().getPriority());
+ }
+
+ if (commonIssueKind == JsonValidationError.FixableIssueKind.ProhibitedType) {
+ final Set<JsonSchemaType> allTypes = errors.stream().map(e -> (JsonValidationError.TypeMismatchIssueData)e.getIssueData())
+ .flatMap(d -> Arrays.stream(d.expectedTypes)).collect(Collectors.toSet());
+
+ if (allTypes.size() == 1) return errors.iterator().next();
+
+ String commonTypeMessage = "Type is not allowed. Expected one of: " + allTypes.stream().map(t -> t.getDescription()).sorted().collect(Collectors.joining(", ")) + ".";
+ return new JsonValidationError(commonTypeMessage, JsonValidationError.FixableIssueKind.TypeMismatch,
+ new JsonValidationError.TypeMismatchIssueData(ContainerUtil.toArray(allTypes, JsonSchemaType[]::new)),
+ errors.iterator().next().getPriority());
+ }
+
+ return null;
+ }
+
+ public boolean isCorrect() {
+ return myErrors.isEmpty();
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaBaseReference.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaBaseReference.java
new file mode 100644
index 00000000..39992c7d
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaBaseReference.java
@@ -0,0 +1,58 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+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 org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Irina.Chernushina on 3/31/2016.
+ */
+public abstract class JsonSchemaBaseReference<T extends PsiElement> extends PsiReferenceBase<T> {
+ 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;
+
+ return isIdenticalTo(that);
+ }
+
+ protected boolean isIdenticalTo(JsonSchemaBaseReference that) {
+ return myElement.equals(that.myElement);
+ }
+
+ @Override
+ public int hashCode() {
+ return myElement.hashCode();
+ }
+
+ private static class MyResolver implements ResolveCache.Resolver {
+ private static final MyResolver INSTANCE = new MyResolver();
+
+ @Override
+ @Nullable
+ public PsiElement resolve(@NotNull PsiReference ref, boolean incompleteCode) {
+ return ((JsonSchemaBaseReference)ref).resolveInner();
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaCompletionContributor.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaCompletionContributor.java
new file mode 100644
index 00000000..cbe848a6
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaCompletionContributor.java
@@ -0,0 +1,672 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInsight.AutoPopupController;
+import com.intellij.codeInsight.completion.*;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.icons.AllIcons;
+import com.intellij.ide.DataManager;
+import com.intellij.internal.statistic.service.fus.collectors.FUSApplicationUsageTrigger;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.actionSystem.IdeActions;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.editor.Caret;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.EditorModificationUtil;
+import com.intellij.openapi.editor.SelectionModel;
+import com.intellij.openapi.editor.actionSystem.CaretSpecificDataContext;
+import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
+import com.intellij.openapi.editor.actionSystem.EditorActionManager;
+import com.intellij.openapi.editor.actions.EditorActionUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.TokenType;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.psi.impl.source.tree.LeafPsiElement;
+import com.intellij.psi.util.PsiUtilCore;
+import com.intellij.util.Consumer;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.ThreeState;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider;
+import com.jetbrains.jsonSchema.extension.SchemaType;
+import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.TestOnly;
+
+import javax.swing.*;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 10/1/2015.
+ */
+public class JsonSchemaCompletionContributor extends CompletionContributor {
+ private static final String BUILTIN_USAGE_KEY = "json.schema.builtin.completion";
+ private static final String SCHEMA_USAGE_KEY = "json.schema.schema.completion";
+ private static final String USER_USAGE_KEY = "json.schema.user.completion";
+ private static final String REMOTE_USAGE_KEY = "json.schema.remote.completion";
+
+ @Override
+ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet result) {
+ final PsiElement position = parameters.getPosition();
+ final VirtualFile file = PsiUtilCore.getVirtualFile(position);
+ if (file == null) return;
+
+ final JsonSchemaService service = JsonSchemaService.Impl.get(position.getProject());
+ if (!service.isApplicableToFile(file)) return;
+ final JsonSchemaObject rootSchema = service.getSchemaObject(file);
+ if (rootSchema == null) return;
+ PsiElement positionParent = position.getParent();
+ if (positionParent != null) {
+ PsiElement parent = positionParent.getParent();
+ if (parent instanceof JsonProperty && "$ref".equals(((JsonProperty)parent).getName()) && service.isSchemaFile(file)) {
+ return;
+ }
+ }
+
+ final VirtualFile schemaFile = rootSchema.getSchemaFile();
+ updateStat(service.getSchemaProvider(schemaFile), schemaFile);
+ doCompletion(parameters, result, rootSchema);
+ }
+
+ public static void doCompletion(@NotNull final CompletionParameters parameters,
+ @NotNull final CompletionResultSet result,
+ @NotNull final JsonSchemaObject rootSchema) {
+ doCompletion(parameters, result, rootSchema, true);
+ }
+
+ public static void doCompletion(@NotNull final CompletionParameters parameters,
+ @NotNull final CompletionResultSet result,
+ @NotNull final JsonSchemaObject rootSchema,
+ boolean stop) {
+ final PsiElement completionPosition = parameters.getOriginalPosition() != null ? parameters.getOriginalPosition() :
+ parameters.getPosition();
+ new Worker(rootSchema, parameters.getPosition(), completionPosition, result).work();
+ if (stop) {
+ result.stopHere();
+ }
+ }
+
+ @TestOnly
+ @NotNull
+ public static List<LookupElement> getCompletionVariants(@NotNull final JsonSchemaObject schema,
+ @NotNull final PsiElement position, @NotNull final PsiElement originalPosition) {
+ final List<LookupElement> result = new ArrayList<>();
+ new Worker(schema, position, originalPosition, element -> result.add(element)).work();
+ return result;
+ }
+
+ private static void updateStat(@Nullable JsonSchemaFileProvider provider, VirtualFile schemaFile) {
+ if (provider == null) {
+ if (schemaFile instanceof HttpVirtualFile) {
+ // auto-detected and auto-downloaded JSON schemas
+ FUSApplicationUsageTrigger usageTrigger = FUSApplicationUsageTrigger.getInstance();
+ usageTrigger.trigger(JsonSchemaUsageTriggerCollector.class, REMOTE_USAGE_KEY);
+ }
+ return;
+ }
+ final SchemaType schemaType = provider.getSchemaType();
+ FUSApplicationUsageTrigger usageTrigger = FUSApplicationUsageTrigger.getInstance();
+ switch (schemaType) {
+ case schema:
+ usageTrigger.trigger(JsonSchemaUsageTriggerCollector.class, SCHEMA_USAGE_KEY);
+ break;
+ case userSchema:
+ usageTrigger.trigger(JsonSchemaUsageTriggerCollector.class, USER_USAGE_KEY);
+ break;
+ case embeddedSchema:
+ usageTrigger.trigger(JsonSchemaUsageTriggerCollector.class, BUILTIN_USAGE_KEY);
+ break;
+ case remoteSchema:
+ // this works only for user-specified remote schemas in our settings, but not for auto-detected remote schemas
+ usageTrigger.trigger(JsonSchemaUsageTriggerCollector.class, REMOTE_USAGE_KEY);
+ break;
+ }
+ }
+
+ private static class Worker {
+ @NotNull private final JsonSchemaObject myRootSchema;
+ @NotNull private final PsiElement myPosition;
+ @NotNull private final PsiElement myOriginalPosition;
+ @NotNull private final Consumer<LookupElement> myResultConsumer;
+ private final boolean myWrapInQuotes;
+ private final boolean myInsideStringLiteral;
+ // we need this set to filter same-named suggestions (they can be suggested by several matching schemes)
+ private final Set<LookupElement> myVariants;
+ private final JsonLikePsiWalker myWalker;
+
+ Worker(@NotNull JsonSchemaObject rootSchema, @NotNull PsiElement position,
+ @NotNull PsiElement originalPosition, @NotNull final Consumer<LookupElement> resultConsumer) {
+ myRootSchema = rootSchema;
+ myPosition = position;
+ myOriginalPosition = originalPosition;
+ myResultConsumer = resultConsumer;
+ myVariants = new HashSet<>();
+ myWalker = JsonLikePsiWalker.getWalker(myPosition, myRootSchema);
+ myWrapInQuotes = myWalker != null && myWalker.isNameQuoted() && !(position.getParent() instanceof JsonStringLiteral);
+ myInsideStringLiteral = position.getParent() instanceof JsonStringLiteral;
+ }
+
+ public void work() {
+ if (myWalker == null) return;
+ final PsiElement checkable = myWalker.goUpToCheckable(myPosition);
+ if (checkable == null) return;
+ final ThreeState isName = myWalker.isName(checkable);
+ final List<JsonSchemaVariantsTreeBuilder.Step> position = myWalker.findPosition(checkable, isName == ThreeState.NO);
+ if (position == null || position.isEmpty() && isName == ThreeState.NO) return;
+
+ final Collection<JsonSchemaObject> schemas = new JsonSchemaResolver(myRootSchema, false, position).resolve();
+ final Set<String> knownNames = ContainerUtil.newHashSet();
+ // too long here, refactor further
+ schemas.forEach(schema -> {
+ if (isName != ThreeState.NO) {
+ final boolean insertComma = myWalker.hasPropertiesBehindAndNoComma(myPosition);
+ final boolean hasValue = myWalker.isPropertyWithValue(checkable);
+
+ final Collection<String> properties = myWalker.getPropertyNamesOfParentObject(myOriginalPosition, myPosition);
+ final JsonPropertyAdapter adapter = myWalker.getParentPropertyAdapter(myOriginalPosition);
+
+ final Map<String, JsonSchemaObject> schemaProperties = schema.getProperties();
+ addAllPropertyVariants(insertComma, hasValue, properties, adapter, schemaProperties, knownNames);
+ addIfThenElsePropertyNameVariants(schema, insertComma, hasValue, properties, adapter, knownNames);
+ }
+
+ if (isName != ThreeState.YES) {
+ suggestValues(schema, isName == ThreeState.NO);
+ }
+ });
+
+ for (LookupElement variant : myVariants) {
+ myResultConsumer.consume(variant);
+ }
+ }
+
+ private void addIfThenElsePropertyNameVariants(@NotNull JsonSchemaObject schema,
+ boolean insertComma,
+ boolean hasValue,
+ @NotNull Collection<String> properties,
+ @Nullable JsonPropertyAdapter adapter,
+ Set<String> knownNames) {
+ if (schema.getIf() == null) return;
+
+ JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(myPosition, schema);
+ JsonPropertyAdapter propertyAdapter = walker == null ? null : walker.getParentPropertyAdapter(myPosition);
+ if (propertyAdapter == null) return;
+
+ JsonObjectValueAdapter object = propertyAdapter.getParentObject();
+ if (object == null) return;
+
+ JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(JsonComplianceCheckerOptions.RELAX_ENUM_CHECK);
+ checker.checkByScheme(object, schema.getIf());
+ if (checker.isCorrect()) {
+ JsonSchemaObject then = schema.getThen();
+ if (then != null) {
+ addAllPropertyVariants(insertComma, hasValue, properties, adapter, then.getProperties(), knownNames);
+ }
+ }
+ else {
+ JsonSchemaObject schemaElse = schema.getElse();
+ if (schemaElse != null) {
+ addAllPropertyVariants(insertComma, hasValue, properties, adapter, schemaElse.getProperties(), knownNames);
+ }
+ }
+ }
+
+ private void addAllPropertyVariants(boolean insertComma,
+ boolean hasValue,
+ Collection<String> properties,
+ JsonPropertyAdapter adapter,
+ Map<String, JsonSchemaObject> schemaProperties, Set<String> knownNames) {
+ schemaProperties.keySet().stream()
+ .filter(name -> !properties.contains(name) && !knownNames.contains(name) || adapter != null && name.equals(adapter.getName()))
+ .forEach(name -> {knownNames.add(name); addPropertyVariant(name, schemaProperties.get(name), hasValue, insertComma);});
+ }
+
+ private void suggestValues(JsonSchemaObject schema, boolean isSurelyValue) {
+ suggestValuesForSchemaVariants(schema.getAnyOf(), isSurelyValue);
+ suggestValuesForSchemaVariants(schema.getOneOf(), isSurelyValue);
+ suggestValuesForSchemaVariants(schema.getAllOf(), isSurelyValue);
+
+ if (schema.getEnum() != null) {
+ for (Object o : schema.getEnum()) {
+ if (myInsideStringLiteral && !(o instanceof String)) continue;
+ addValueVariant(o.toString(), null);
+ }
+ }
+ else if (isSurelyValue) {
+ final JsonSchemaType type = schema.guessType();
+ suggestSpecialValues(type);
+ if (type != null) {
+ suggestByType(schema, type);
+ } else if (schema.getTypeVariants() != null) {
+ for (JsonSchemaType schemaType : schema.getTypeVariants()) {
+ suggestByType(schema, schemaType);
+ }
+ }
+ }
+ }
+
+ private void suggestSpecialValues(@Nullable JsonSchemaType type) {
+ if (JsonSchemaVersion.isSchemaSchemaId(myRootSchema.getId()) && type == JsonSchemaType._string) {
+ JsonPropertyAdapter propertyAdapter = myWalker.getParentPropertyAdapter(myOriginalPosition);
+ if (propertyAdapter == null || !"required".equals(propertyAdapter.getName())) return;
+ PsiElement checkable = myWalker.goUpToCheckable(myPosition);
+ if (!(checkable instanceof JsonStringLiteral) && !(checkable instanceof JsonReferenceExpression)) return;
+ JsonObject propertiesObject = JsonRequiredPropsReferenceProvider.findPropertiesObject(checkable);
+ if (propertiesObject == null) return;
+ PsiElement parent = checkable.getParent();
+ Set<String> items = parent instanceof JsonArray ? ((JsonArray)parent).getValueList().stream()
+ .filter(v -> v instanceof JsonStringLiteral).map(v -> ((JsonStringLiteral)v).getValue()).collect(Collectors.toSet()) : ContainerUtil.newHashSet();
+ propertiesObject.getPropertyList().stream().map(p -> p.getName()).filter(n -> !items.contains(n)).forEach(n -> addStringVariant(n));
+ }
+ }
+
+ private void suggestByType(JsonSchemaObject schema, JsonSchemaType type) {
+ if (JsonSchemaType._string.equals(type)) {
+ addPossibleStringValue(schema);
+ }
+ if (myInsideStringLiteral){
+ return;
+ }
+ if (JsonSchemaType._boolean.equals(type)) {
+ addPossibleBooleanValue(type);
+ } else if (JsonSchemaType._null.equals(type)) {
+ addValueVariant("null", null);
+ } else if (JsonSchemaType._array.equals(type)) {
+ String value = myWalker.getDefaultArrayValue();
+ addValueVariant(value, null,
+ myWalker.defaultArrayValueDescription(), createArrayOrObjectLiteralInsertHandler(myWalker.invokeEnterBeforeObjectAndArray(), value.length()));
+ } else if (JsonSchemaType._object.equals(type)) {
+ String value = myWalker.getDefaultObjectValue();
+ addValueVariant(value, null,
+ myWalker.defaultObjectValueDescription(), createArrayOrObjectLiteralInsertHandler(myWalker.invokeEnterBeforeObjectAndArray(), value.length()));
+ }
+ }
+
+ private void addPossibleStringValue(JsonSchemaObject schema) {
+ Object defaultValue = schema.getDefault();
+ String defaultValueString = defaultValue == null ? null : defaultValue.toString();
+ addStringVariant(defaultValueString);
+ }
+
+ private void addStringVariant(String defaultValueString) {
+ if (!StringUtil.isEmpty(defaultValueString)) {
+ String normalizedValue = defaultValueString;
+ boolean shouldQuote = myWalker.quotesForStringLiterals();
+ boolean isQuoted = StringUtil.isQuotedString(normalizedValue);
+ if (shouldQuote && !isQuoted) {
+ normalizedValue = StringUtil.wrapWithDoubleQuote(normalizedValue);
+ }
+ else if (!shouldQuote && isQuoted) {
+ normalizedValue = StringUtil.unquoteString(normalizedValue);
+ }
+ addValueVariant(normalizedValue, null);
+ }
+ }
+
+ private void suggestValuesForSchemaVariants(List<JsonSchemaObject> list, boolean isSurelyValue) {
+ if (list != null && list.size() > 0) {
+ for (JsonSchemaObject schemaObject : list) {
+ suggestValues(schemaObject, isSurelyValue);
+ }
+ }
+ }
+
+ private void addPossibleBooleanValue(JsonSchemaType type) {
+ if (JsonSchemaType._boolean.equals(type)) {
+ addValueVariant("true", null);
+ addValueVariant("false", null);
+ }
+ }
+
+
+ private void addValueVariant(@NotNull String key, @SuppressWarnings("SameParameterValue") @Nullable final String description) {
+ addValueVariant(key, description, null, null);
+ }
+
+ private void addValueVariant(@NotNull String key,
+ @SuppressWarnings("SameParameterValue") @Nullable final String description,
+ @Nullable final String altText,
+ @Nullable InsertHandler<LookupElement> handler) {
+ LookupElementBuilder builder = LookupElementBuilder.create(!myWrapInQuotes ? StringUtil.unquoteString(key) : key);
+ if (altText != null) {
+ builder = builder.withPresentableText(altText);
+ }
+ if (description != null) {
+ builder = builder.withTypeText(description);
+ }
+ if (handler != null) {
+ builder = builder.withInsertHandler(handler);
+ }
+ myVariants.add(builder);
+ }
+
+ private void addPropertyVariant(@NotNull String key, @NotNull JsonSchemaObject jsonSchemaObject, boolean hasValue, boolean insertComma) {
+ final Collection<JsonSchemaObject> variants = new JsonSchemaResolver(jsonSchemaObject).resolve();
+ jsonSchemaObject = ObjectUtils.coalesce(ContainerUtil.getFirstItem(variants), jsonSchemaObject);
+ key = !myWrapInQuotes ? key : StringUtil.wrapWithDoubleQuote(key);
+ LookupElementBuilder builder = LookupElementBuilder.create(key);
+
+ final String typeText = JsonSchemaDocumentationProvider.getBestDocumentation(true, jsonSchemaObject);
+ if (!StringUtil.isEmptyOrSpaces(typeText)) {
+ final String text = StringUtil.removeHtmlTags(typeText);
+ final int firstSentenceMark = text.indexOf(". ");
+ builder = builder.withTypeText(firstSentenceMark == -1 ? text : text.substring(0, firstSentenceMark + 1), true);
+ }
+ else {
+ String type = jsonSchemaObject.getTypeDescription(true);
+ if (type != null) {
+ builder = builder.withTypeText(type, true);
+ }
+ }
+
+ builder = builder.withIcon(getIcon(jsonSchemaObject.guessType()));
+
+ if (hasSameType(variants)) {
+ final JsonSchemaType type = jsonSchemaObject.guessType();
+ final List<Object> values = jsonSchemaObject.getEnum();
+ Object defaultValue = jsonSchemaObject.getDefault();
+
+ boolean hasValues = !ContainerUtil.isEmpty(values);
+ if (type != null || hasValues || defaultValue != null) {
+ builder = builder.withInsertHandler(
+ !hasValues || values.stream().map(v -> v.getClass()).distinct().count() == 1 ?
+ createPropertyInsertHandler(jsonSchemaObject, hasValue, insertComma) :
+ createDefaultPropertyInsertHandler(true, insertComma));
+ }
+ else {
+ builder = builder.withInsertHandler(createDefaultPropertyInsertHandler(false, insertComma));
+ }
+ } else if (!hasValue) {
+ builder = builder.withInsertHandler(createDefaultPropertyInsertHandler(false, insertComma));
+ }
+
+ myVariants.add(builder);
+ }
+
+ @NotNull
+ private static Icon getIcon(@Nullable JsonSchemaType type) {
+ if (type == null) return AllIcons.Nodes.Property;
+ switch (type) {
+ case _object:
+ return AllIcons.Json.Object;
+ case _array:
+ return AllIcons.Json.Array;
+ default:
+ return AllIcons.Nodes.Property;
+ }
+ }
+
+ private static boolean hasSameType(@NotNull Collection<JsonSchemaObject> variants) {
+ return variants.stream().map(JsonSchemaObject::guessType).filter(Objects::nonNull).distinct().count() <= 1;
+ }
+
+ private static InsertHandler<LookupElement> createArrayOrObjectLiteralInsertHandler(boolean newline, int insertedTextSize) {
+ return new InsertHandler<LookupElement>() {
+ @Override
+ public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) {
+ Editor editor = context.getEditor();
+
+ if (!newline) {
+ EditorModificationUtil.moveCaretRelatively(editor, -1);
+ }
+ else {
+ EditorModificationUtil.moveCaretRelatively(editor, -insertedTextSize);
+ PsiDocumentManager.getInstance(context.getProject()).commitDocument(editor.getDocument());
+ invokeEnterHandler(editor);
+ EditorActionUtil.moveCaretToLineEnd(editor, false, false);
+ }
+ AutoPopupController.getInstance(context.getProject()).autoPopupMemberLookup(editor, null);
+ }
+ };
+ }
+
+ private InsertHandler<LookupElement> createDefaultPropertyInsertHandler(@SuppressWarnings("SameParameterValue") boolean hasValue,
+ boolean insertComma) {
+ return new InsertHandler<LookupElement>() {
+ @Override
+ public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) {
+ ApplicationManager.getApplication().assertWriteAccessAllowed();
+ Editor editor = context.getEditor();
+ Project project = context.getProject();
+
+ if (handleInsideQuotesInsertion(context, editor, hasValue)) return;
+
+ // inserting longer string for proper formatting
+ final String stringToInsert = ": 1" + (insertComma ? "," : "");
+ EditorModificationUtil.insertStringAtCaret(editor, stringToInsert, false, true, 2);
+ formatInsertedString(context, stringToInsert.length());
+ final int offset = editor.getCaretModel().getOffset();
+ context.getDocument().deleteString(offset, offset + 1);
+ PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());
+ AutoPopupController.getInstance(context.getProject()).autoPopupMemberLookup(context.getEditor(), null);
+ }
+ };
+ }
+
+ @NotNull
+ private InsertHandler<LookupElement> createPropertyInsertHandler(@NotNull JsonSchemaObject jsonSchemaObject,
+ final boolean hasValue,
+ boolean insertComma) {
+ JsonSchemaType type = jsonSchemaObject.guessType();
+ List<Object> values = jsonSchemaObject.getEnum();
+ if (type == null && values != null && !values.isEmpty()) type = detectType(values);
+ final Object defaultValue = jsonSchemaObject.getDefault();
+ final String defaultValueAsString = defaultValue == null || defaultValue instanceof JsonSchemaObject ? null :
+ (defaultValue instanceof String ? "\"" + defaultValue + "\"" :
+ String.valueOf(defaultValue));
+ JsonSchemaType finalType = type;
+ return new InsertHandler<LookupElement>() {
+ @Override
+ public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) {
+ ApplicationManager.getApplication().assertWriteAccessAllowed();
+ Editor editor = context.getEditor();
+ Project project = context.getProject();
+ String stringToInsert = null;
+ final String comma = insertComma ? "," : "";
+
+ if (handleInsideQuotesInsertion(context, editor, hasValue)) return;
+
+ PsiElement element = context.getFile().findElementAt(editor.getCaretModel().getOffset());
+ boolean insertColon = element == null || !":".equals(element.getText());
+ if (!insertColon) {
+ editor.getCaretModel().moveToOffset(editor.getCaretModel().getOffset() + 1);
+ }
+
+ if (finalType != null) {
+ boolean hadEnter;
+ switch (finalType) {
+ case _object:
+ EditorModificationUtil.insertStringAtCaret(editor, insertColon ? ": " : " ",
+ false, true,
+ insertColon ? 2 : 1);
+ hadEnter = false;
+ boolean invokeEnter = myWalker.invokeEnterBeforeObjectAndArray();
+ if (insertColon && invokeEnter) {
+ invokeEnterHandler(editor);
+ hadEnter = true;
+ }
+ if (insertColon) {
+ stringToInsert = myWalker.getDefaultObjectValue() + comma;
+ EditorModificationUtil.insertStringAtCaret(editor, stringToInsert,
+ false, true,
+ hadEnter ? 0 : 1);
+ }
+
+ if (hadEnter || !insertColon) {
+ EditorActionUtil.moveCaretToLineEnd(editor, false, false);
+ }
+
+ PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());
+ if (!hadEnter && stringToInsert != null) {
+ formatInsertedString(context, stringToInsert.length());
+ }
+ if (stringToInsert != null && !invokeEnter) {
+ invokeEnterHandler(editor);
+ }
+ break;
+ case _boolean:
+ String value = String.valueOf(Boolean.TRUE.toString().equals(defaultValueAsString));
+ stringToInsert = (insertColon ? ": " : " ") + value + comma;
+ SelectionModel model = editor.getSelectionModel();
+
+ EditorModificationUtil.insertStringAtCaret(editor, stringToInsert,
+ false, true,
+ stringToInsert.length() - comma.length());
+ formatInsertedString(context, stringToInsert.length());
+ int start = editor.getSelectionModel().getSelectionStart();
+ model.setSelection(start - value.length(), start);
+ AutoPopupController.getInstance(context.getProject()).autoPopupMemberLookup(context.getEditor(), null);
+ break;
+ case _array:
+ EditorModificationUtil.insertStringAtCaret(editor, insertColon ? ": " : " ",
+ false, true,
+ insertColon ? 2 : 1);
+ hadEnter = false;
+ if (insertColon && myWalker.invokeEnterBeforeObjectAndArray()) {
+ invokeEnterHandler(editor);
+ hadEnter = true;
+ }
+ if (insertColon) {
+ stringToInsert = myWalker.getDefaultArrayValue() + comma;
+ EditorModificationUtil.insertStringAtCaret(editor, stringToInsert,
+ false, true,
+ hadEnter ? 0 : 1);
+ }
+ if (hadEnter) {
+ EditorActionUtil.moveCaretToLineEnd(editor, false, false);
+ }
+
+ PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());
+
+ if (stringToInsert != null) {
+ formatInsertedString(context, stringToInsert.length());
+ }
+ break;
+ case _string:
+ case _integer:
+ insertPropertyWithEnum(context, editor, defaultValueAsString, values, finalType, comma, myWalker, insertColon);
+ break;
+ default:
+ }
+ }
+ else {
+ insertPropertyWithEnum(context, editor, defaultValueAsString, values, null, comma, myWalker, insertColon);
+ }
+ }
+ };
+ }
+
+ private static void invokeEnterHandler(Editor editor) {
+ EditorActionHandler handler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_ENTER);
+ Caret caret = editor.getCaretModel().getCurrentCaret();
+ handler.execute(editor, caret,
+ new CaretSpecificDataContext(DataManager.getInstance().getDataContext(editor.getContentComponent()), caret));
+ }
+
+ private boolean handleInsideQuotesInsertion(@NotNull InsertionContext context, @NotNull Editor editor, boolean hasValue) {
+ if (myInsideStringLiteral) {
+ int offset = editor.getCaretModel().getOffset();
+ PsiElement element = context.getFile().findElementAt(offset);
+ int tailOffset = context.getTailOffset();
+ int guessEndOffset = tailOffset + 1;
+ if (element instanceof LeafPsiElement) {
+ if (handleIncompleteString(editor, element)) return false;
+ int endOffset = element.getTextRange().getEndOffset();
+ if (endOffset > tailOffset) {
+ context.getDocument().deleteString(tailOffset, endOffset - 1);
+ }
+ }
+ if (hasValue) {
+ return true;
+ }
+ editor.getCaretModel().moveToOffset(guessEndOffset);
+ } else editor.getCaretModel().moveToOffset(context.getTailOffset());
+ return false;
+ }
+
+ private static boolean handleIncompleteString(@NotNull Editor editor, @NotNull PsiElement element) {
+ if (((LeafPsiElement)element).getElementType() == TokenType.WHITE_SPACE) {
+ PsiElement prevSibling = element.getPrevSibling();
+ if (prevSibling instanceof JsonProperty) {
+ JsonValue nameElement = ((JsonProperty)prevSibling).getNameElement();
+ if (!nameElement.getText().endsWith("\"")) {
+ editor.getCaretModel().moveToOffset(nameElement.getTextRange().getEndOffset());
+ EditorModificationUtil.insertStringAtCaret(editor, "\"", false, true, 1);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ private static JsonSchemaType detectType(List<Object> values) {
+ JsonSchemaType type = null;
+ for (Object value : values) {
+ JsonSchemaType newType = null;
+ if (value instanceof Integer) newType = JsonSchemaType._integer;
+ if (type != null && !type.equals(newType)) return null;
+ type = newType;
+ }
+ return type;
+ }
+ }
+
+ private static void insertPropertyWithEnum(InsertionContext context,
+ Editor editor,
+ String defaultValue,
+ List<Object> values,
+ JsonSchemaType type,
+ String comma,
+ JsonLikePsiWalker walker,
+ boolean insertColon) {
+ if (!walker.quotesForStringLiterals() && defaultValue != null) {
+ defaultValue = StringUtil.unquoteString(defaultValue);
+ }
+ final boolean isNumber = type != null && (JsonSchemaType._integer.equals(type) || JsonSchemaType._number.equals(type)) ||
+ type == null && (defaultValue != null &&
+ !StringUtil.isQuotedString(defaultValue) || values != null && ContainerUtil.and(values, v -> !(v instanceof String)));
+ boolean hasValues = !ContainerUtil.isEmpty(values);
+ boolean hasDefaultValue = !StringUtil.isEmpty(defaultValue);
+ boolean hasQuotes = isNumber || !walker.quotesForStringLiterals();
+ final String colonWs = insertColon ? ": " : " ";
+ String stringToInsert = colonWs + (hasDefaultValue ? defaultValue : (hasQuotes ? "" : "\"\"")) + comma;
+ EditorModificationUtil.insertStringAtCaret(editor, stringToInsert, false, true,
+ insertColon ? 2 : 1);
+ if (!hasQuotes || hasDefaultValue) {
+ SelectionModel model = editor.getSelectionModel();
+ int caretStart = model.getSelectionStart();
+ int newOffset = caretStart + (hasDefaultValue ? defaultValue.length() : 1);
+ if (hasDefaultValue && !hasQuotes) newOffset--;
+ model.setSelection(hasQuotes ? caretStart : (caretStart + 1), newOffset);
+ editor.getCaretModel().moveToOffset(newOffset);
+ }
+
+ if (!walker.invokeEnterBeforeObjectAndArray() && !stringToInsert.equals(colonWs + comma)) {
+ formatInsertedString(context, stringToInsert.length());
+ }
+
+ if (hasValues) {
+ AutoPopupController.getInstance(context.getProject()).autoPopupMemberLookup(context.getEditor(), null);
+ }
+ }
+
+ public static void formatInsertedString(@NotNull InsertionContext context,
+ int offset) {
+ Project project = context.getProject();
+ PsiDocumentManager.getInstance(project).commitDocument(context.getDocument());
+ CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project);
+ codeStyleManager.reformatText(context.getFile(), context.getStartOffset(), context.getTailOffset() + offset);
+ }
+} \ No newline at end of file
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaComplianceChecker.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaComplianceChecker.java
new file mode 100644
index 00000000..12e52c43
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaComplianceChecker.java
@@ -0,0 +1,157 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInspection.LocalInspectionToolSession;
+import com.intellij.codeInspection.LocalQuickFix;
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class JsonSchemaComplianceChecker {
+ private static final Key<Set<PsiElement>> ANNOTATED_PROPERTIES = Key.create("JsonSchema.Properties.Annotated");
+
+ @NotNull private final JsonSchemaObject myRootSchema;
+ @NotNull private final ProblemsHolder myHolder;
+ @NotNull private final JsonLikePsiWalker myWalker;
+ private final LocalInspectionToolSession mySession;
+ @NotNull private final JsonComplianceCheckerOptions myOptions;
+ @Nullable private final String myMessagePrefix;
+
+ public JsonSchemaComplianceChecker(@NotNull JsonSchemaObject rootSchema,
+ @NotNull ProblemsHolder holder,
+ @NotNull JsonLikePsiWalker walker,
+ @NotNull LocalInspectionToolSession session,
+ @NotNull JsonComplianceCheckerOptions options) {
+ this(rootSchema, holder, walker, session, options, null);
+ }
+
+ public JsonSchemaComplianceChecker(@NotNull JsonSchemaObject rootSchema,
+ @NotNull ProblemsHolder holder,
+ @NotNull JsonLikePsiWalker walker,
+ @NotNull LocalInspectionToolSession session,
+ @NotNull JsonComplianceCheckerOptions options,
+ @Nullable String messagePrefix) {
+ myRootSchema = rootSchema;
+ myHolder = holder;
+ myWalker = walker;
+ mySession = session;
+ myOptions = options;
+ myMessagePrefix = messagePrefix;
+ }
+
+ public void annotate(@NotNull final PsiElement element) {
+ final JsonPropertyAdapter firstProp = myWalker.getParentPropertyAdapter(element);
+ if (firstProp != null) {
+ final List<JsonSchemaVariantsTreeBuilder.Step> position = myWalker.findPosition(firstProp.getDelegate(), true);
+ if (position == null || position.isEmpty()) return;
+ final MatchResult result = new JsonSchemaResolver(myRootSchema, false, position).detailedResolve();
+ for (JsonValueAdapter value : firstProp.getValues()) {
+ createWarnings(JsonSchemaAnnotatorChecker.checkByMatchResult(value, result, myOptions));
+ }
+ }
+ checkRoot(element, firstProp);
+ }
+
+ private void checkRoot(@NotNull PsiElement element, @Nullable JsonPropertyAdapter firstProp) {
+ JsonValueAdapter rootToCheck;
+ if (firstProp == null) {
+ rootToCheck = findTopLevelElement(myWalker, element);
+ } else {
+ rootToCheck = firstProp.getParentObject();
+ if (rootToCheck == null || !myWalker.isTopJsonElement(rootToCheck.getDelegate().getParent())) {
+ return;
+ }
+ }
+ if (rootToCheck != null) {
+ final MatchResult matchResult = new JsonSchemaResolver(myRootSchema).detailedResolve();
+ createWarnings(JsonSchemaAnnotatorChecker.checkByMatchResult(rootToCheck, matchResult, myOptions));
+ }
+ }
+
+ private void createWarnings(@Nullable JsonSchemaAnnotatorChecker checker) {
+ if (checker == null || checker.isCorrect()) return;
+ // compute intersecting ranges - we'll solve warning priorities based on this information
+ List<TextRange> ranges = ContainerUtil.newArrayList();
+ List<List<Map.Entry<PsiElement, JsonValidationError>>> entries = ContainerUtil.newArrayList();
+ for (Map.Entry<PsiElement, JsonValidationError> entry : checker.getErrors().entrySet()) {
+ TextRange range = entry.getKey().getTextRange();
+ boolean processed = false;
+ for (int i = 0; i < ranges.size(); i++) {
+ TextRange currRange = ranges.get(i);
+ if (currRange.intersects(range)) {
+ ranges.set(i, new TextRange(Math.min(currRange.getStartOffset(), range.getStartOffset()), Math.max(currRange.getEndOffset(), range.getEndOffset())));
+ entries.get(i).add(entry);
+ processed = true;
+ break;
+ }
+ }
+ if (processed) continue;
+
+ ranges.add(range);
+ entries.add(ContainerUtil.newArrayList(entry));
+ }
+
+ // for each set of intersecting ranges, compute the best errors to show
+ for (List<Map.Entry<PsiElement, JsonValidationError>> entryList : entries) {
+ int min = entryList.stream().map(v -> v.getValue().getPriority().ordinal()).min(Integer::compareTo).orElse(Integer.MAX_VALUE);
+ for (Map.Entry<PsiElement, JsonValidationError> entry : entryList) {
+ JsonValidationError validationError = entry.getValue();
+ PsiElement psiElement = entry.getKey();
+ if (validationError.getPriority().ordinal() > min) {
+ continue;
+ }
+ TextRange range = myWalker.adjustErrorHighlightingRange(psiElement);
+ range = range.shiftLeft(psiElement.getTextRange().getStartOffset());
+ registerError(psiElement, range, validationError);
+ }
+ }
+ }
+
+ private void registerError(@NotNull PsiElement psiElement, @NotNull TextRange range, @NotNull JsonValidationError validationError) {
+ if (checkIfAlreadyProcessed(psiElement)) return;
+ String value = validationError.getMessage();
+ if (myMessagePrefix != null) value = myMessagePrefix + value;
+ LocalQuickFix[] fix = validationError.createFixes(myWalker.getQuickFixAdapter(myHolder.getProject()));
+ if (fix.length == 0) {
+ myHolder.registerProblem(psiElement, range, value);
+ }
+ else {
+ myHolder.registerProblem(psiElement, range, value, fix);
+ }
+ }
+
+ private static JsonValueAdapter findTopLevelElement(@NotNull JsonLikePsiWalker walker, @NotNull PsiElement element) {
+ final Ref<PsiElement> ref = new Ref<>();
+ PsiTreeUtil.findFirstParent(element, el -> {
+ final boolean isTop = walker.isTopJsonElement(el);
+ if (!isTop) ref.set(el);
+ return isTop;
+ });
+ return ref.isNull() ? null : walker.createValueAdapter(ref.get());
+ }
+
+ private boolean checkIfAlreadyProcessed(@NotNull PsiElement property) {
+ Set<PsiElement> data = mySession.getUserData(ANNOTATED_PROPERTIES);
+ if (data == null) {
+ data = new HashSet<>();
+ mySession.putUserData(ANNOTATED_PROPERTIES, data);
+ }
+ if (data.contains(property)) return true;
+ data.add(property);
+ return false;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaConflictNotificationProvider.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaConflictNotificationProvider.java
new file mode 100644
index 00000000..04c09278
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaConflictNotificationProvider.java
@@ -0,0 +1,120 @@
+/*
+ * 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.fileEditor.FileEditor;
+import com.intellij.openapi.options.ShowSettingsUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.EditorNotificationPanel;
+import com.intellij.ui.EditorNotifications;
+import com.intellij.ui.LightColors;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider;
+import com.jetbrains.jsonSchema.extension.SchemaType;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.settings.mappings.JsonSchemaMappingsConfigurable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 2/19/2016.
+ */
+public class JsonSchemaConflictNotificationProvider extends EditorNotifications.Provider<EditorNotificationPanel> {
+ private static final Key<EditorNotificationPanel> KEY = Key.create("json.schema.conflict.notification.panel");
+
+ @NotNull
+ private final Project myProject;
+ @NotNull
+ private final JsonSchemaService myJsonSchemaService;
+
+ public JsonSchemaConflictNotificationProvider(@NotNull Project project,
+ @NotNull JsonSchemaService jsonSchemaService) {
+ myProject = project;
+ myJsonSchemaService = jsonSchemaService;
+ }
+
+ @NotNull
+ @Override
+ public Key<EditorNotificationPanel> getKey() {
+ return KEY;
+ }
+
+ @Nullable
+ @Override
+ public EditorNotificationPanel createNotificationPanel(@NotNull VirtualFile file, @NotNull FileEditor fileEditor) {
+ if (!myJsonSchemaService.isApplicableToFile(file)) return null;
+ final Collection<VirtualFile> schemaFiles = ContainerUtil.newArrayList();
+ if (!hasConflicts(schemaFiles, file)) return null;
+
+ final String message = createMessage(schemaFiles, myJsonSchemaService,
+ "; ", "<html>There are several JSON Schemas mapped to this file: ", "</html>");
+ if (message == null) return null;
+
+ final EditorNotificationPanel panel = new EditorNotificationPanel(LightColors.RED);
+ panel.setText(message);
+ panel.createActionLabel("Edit JSON Schema Mappings", () -> {
+ ShowSettingsUtil.getInstance().editConfigurable(myProject, new JsonSchemaMappingsConfigurable(myProject));
+ EditorNotifications.getInstance(myProject).updateNotifications(file);
+ });
+ return panel;
+ }
+
+ private boolean hasConflicts(@NotNull Collection<VirtualFile> files, @NotNull VirtualFile file) {
+ List<JsonSchemaFileProvider> providers = ((JsonSchemaServiceImpl)myJsonSchemaService).getProvidersForFile(file);
+ for (JsonSchemaFileProvider provider : providers) {
+ if (provider.getSchemaType() != SchemaType.userSchema) continue;
+ VirtualFile schemaFile = provider.getSchemaFile();
+ if (schemaFile != null) {
+ files.add(schemaFile);
+ }
+ }
+ return files.size() > 1;
+ }
+
+ public static String createMessage(@NotNull final Collection<? extends VirtualFile> schemaFiles,
+ @NotNull JsonSchemaService jsonSchemaService,
+ @NotNull String separator,
+ @NotNull String prefix,
+ @NotNull String suffix) {
+ final List<Pair<Boolean, String>> pairList = schemaFiles.stream()
+ .map(file -> jsonSchemaService.getSchemaProvider(file))
+ .filter(Objects::nonNull)
+ .map(provider -> Pair.create(SchemaType.userSchema.equals(provider.getSchemaType()), provider.getName()))
+ .collect(Collectors.toList());
+
+ final long numOfSystemSchemas = pairList.stream().filter(pair -> !pair.getFirst()).count();
+ // do not report anything if there is only one system schema and one user schema (user overrides schema that we provide)
+ if (pairList.size() == 2 && numOfSystemSchemas == 1) return null;
+
+ final boolean withTypes = numOfSystemSchemas > 0;
+ return pairList.stream().map(pair -> {
+ if (withTypes) {
+ return String.format("%s schema '%s'", Boolean.TRUE.equals(pair.getFirst()) ? "user" : "system", pair.getSecond());
+ }
+ else {
+ return pair.getSecond();
+ }
+ }).collect(Collectors.joining(separator, prefix, suffix));
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaDocumentationProvider.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaDocumentationProvider.java
new file mode 100644
index 00000000..4bcb0732
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaDocumentationProvider.java
@@ -0,0 +1,218 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.lang.documentation.DocumentationMarkup;
+import com.intellij.lang.documentation.DocumentationProvider;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.*;
+import com.intellij.psi.impl.FakePsiElement;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+
+
+public class JsonSchemaDocumentationProvider implements DocumentationProvider {
+ @Nullable
+ @Override
+ public String getQuickNavigateInfo(PsiElement element, PsiElement originalElement) {
+ return findSchemaAndGenerateDoc(element, originalElement, true, null);
+ }
+
+ @Nullable
+ @Override
+ public List<String> getUrlFor(PsiElement element, PsiElement originalElement) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String generateDoc(PsiElement element, @Nullable PsiElement originalElement) {
+ String forcedPropName = null;
+ if (element instanceof FakeDocElement) {
+ forcedPropName = ((FakeDocElement)element).myAltName;
+ element = ((FakeDocElement)element).myContextElement;
+ }
+ return findSchemaAndGenerateDoc(element, originalElement, false, forcedPropName);
+ }
+
+ @Nullable
+ public static String findSchemaAndGenerateDoc(PsiElement element,
+ @Nullable PsiElement originalElement,
+ final boolean preferShort,
+ @Nullable String forcedPropName) {
+ if (element instanceof FakeDocElement) return null;
+ element = isWhitespaceOrComment(originalElement) ? element : ObjectUtils.coalesce(originalElement, element);
+ final PsiFile containingFile = element.getContainingFile();
+ if (containingFile == null) return null;
+ final JsonSchemaService service = JsonSchemaService.Impl.get(element.getProject());
+ VirtualFile virtualFile = containingFile.getViewProvider().getVirtualFile();
+ if (!service.isApplicableToFile(virtualFile)) return null;
+ final JsonSchemaObject rootSchema = service.getSchemaObject(virtualFile);
+ if (rootSchema == null) return null;
+
+ return generateDoc(element, rootSchema, preferShort, forcedPropName);
+ }
+
+ private static boolean isWhitespaceOrComment(@Nullable PsiElement originalElement) {
+ return originalElement instanceof PsiWhiteSpace || originalElement instanceof PsiComment;
+ }
+
+ @Nullable
+ public static String generateDoc(@NotNull final PsiElement element,
+ @NotNull final JsonSchemaObject rootSchema,
+ final boolean preferShort,
+ @Nullable String forcedPropName) {
+ final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(element, rootSchema);
+ if (walker == null) return null;
+
+ final PsiElement checkable = walker.goUpToCheckable(element);
+ if (checkable == null) return null;
+ final List<JsonSchemaVariantsTreeBuilder.Step> position = walker.findPosition(checkable, true);
+ if (position == null) return null;
+ if (forcedPropName != null) {
+ if (isWhitespaceOrComment(element)) {
+ position.add(JsonSchemaVariantsTreeBuilder.Step.createPropertyStep(forcedPropName));
+ }
+ else {
+ if (position.isEmpty()) {
+ return null;
+ }
+ final JsonSchemaVariantsTreeBuilder.Step lastStep = position.get(position.size() - 1);
+ if (lastStep.getName() == null) return null;
+ position.set(position.size() - 1, JsonSchemaVariantsTreeBuilder.Step.createPropertyStep(forcedPropName));
+ }
+ }
+ final Collection<JsonSchemaObject> schemas = new JsonSchemaResolver(rootSchema, true, position).resolve();
+
+ String htmlDescription = null;
+ List<JsonSchemaType> possibleTypes = ContainerUtil.newArrayList();
+ for (JsonSchemaObject schema : schemas) {
+ if (htmlDescription == null) {
+ htmlDescription = getBestDocumentation(preferShort, schema);
+ }
+ if (schema.getType() != null && schema.getType() != JsonSchemaType._any) {
+ possibleTypes.add(schema.getType());
+ }
+ else if (schema.getTypeVariants() != null) {
+ possibleTypes.addAll(schema.getTypeVariants());
+ }
+ }
+
+ return htmlDescription == null
+ ? null
+ : appendNameTypeAndApi(position, getThirdPartyApiInfo(element, rootSchema), possibleTypes, htmlDescription, preferShort);
+ }
+
+ @NotNull
+ private static String appendNameTypeAndApi(@NotNull List<JsonSchemaVariantsTreeBuilder.Step> position,
+ @NotNull String apiInfo,
+ @NotNull List<JsonSchemaType> possibleTypes,
+ @NotNull String htmlDescription, boolean preferShort) {
+ if (position.size() == 0) return htmlDescription;
+
+ JsonSchemaVariantsTreeBuilder.Step lastStep = position.get(position.size() - 1);
+ String name = lastStep.getName();
+ if (name == null) return htmlDescription;
+
+ String type = "";
+ String schemaType = JsonSchemaObject.getTypesDescription(false, possibleTypes);
+ if (schemaType != null) {
+ type = ": " + schemaType;
+ }
+
+ if (preferShort) {
+ htmlDescription = "<b>" + name + "</b>" + type + apiInfo + "<br/>" + htmlDescription;
+ }
+ else {
+ htmlDescription = DocumentationMarkup.DEFINITION_START + name + type + apiInfo + DocumentationMarkup.DEFINITION_END +
+ DocumentationMarkup.CONTENT_START + htmlDescription + DocumentationMarkup.CONTENT_END;
+ }
+ return htmlDescription;
+ }
+
+ @NotNull
+ private static String getThirdPartyApiInfo(@NotNull PsiElement element,
+ @NotNull JsonSchemaObject rootSchema) {
+ JsonSchemaService service = JsonSchemaService.Impl.get(element.getProject());
+ String apiInfo = "";
+ JsonSchemaFileProvider provider = service.getSchemaProvider(rootSchema.getSchemaFile());
+ if (provider != null) {
+ String information = provider.getThirdPartyApiInformation();
+ if (information != null) {
+ apiInfo = "&nbsp;&nbsp;<i>(" + information + ")</i>";
+ }
+ }
+ return apiInfo;
+ }
+
+ @Nullable
+ public static String getBestDocumentation(boolean preferShort, @NotNull final JsonSchemaObject schema) {
+ final String htmlDescription = schema.getHtmlDescription();
+ final String description = schema.getDescription();
+ final String title = schema.getTitle();
+ if (preferShort && !StringUtil.isEmptyOrSpaces(title)) {
+ return plainTextPostProcess(title);
+ } else if (!StringUtil.isEmptyOrSpaces(htmlDescription)) {
+ String desc = htmlDescription;
+ if (!StringUtil.isEmptyOrSpaces(title)) desc = plainTextPostProcess(title) + "<br/>" + desc;
+ return desc;
+ } else if (!StringUtil.isEmptyOrSpaces(description)) {
+ String desc = plainTextPostProcess(description);
+ if (!StringUtil.isEmptyOrSpaces(title)) desc = plainTextPostProcess(title) + "<br/>" + desc;
+ return desc;
+ }
+ return null;
+ }
+
+ @NotNull
+ private static String plainTextPostProcess(String text) {
+ return StringUtil.escapeXml(text).replace("\\n", "<br/>");
+ }
+
+ @Nullable
+ @Override
+ public PsiElement getDocumentationElementForLookupItem(PsiManager psiManager, Object object, PsiElement element) {
+ if ((element instanceof JsonProperty || isWhitespaceOrComment(element) && element.getParent() instanceof JsonObject) && object instanceof String) {
+ return new FakeDocElement(element instanceof JsonProperty ? ((JsonProperty)element).getNameElement() : element, StringUtil.unquoteString((String)object));
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public PsiElement getDocumentationElementForLink(PsiManager psiManager, String link, PsiElement context) {
+ return null;
+ }
+
+ private static class FakeDocElement extends FakePsiElement {
+ private final PsiElement myContextElement;
+ private final String myAltName;
+
+ private FakeDocElement(PsiElement context, String name) {
+ myContextElement = context;
+ myAltName = name;
+ }
+
+ @Override
+ public PsiElement getParent() {
+ return myContextElement;
+ }
+
+ @NotNull
+ @Override
+ public TextRange getTextRangeInParent() {
+ return myContextElement.getTextRange().shiftLeft(myContextElement.getTextOffset());
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndex.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndex.java
new file mode 100644
index 00000000..e4827358
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndex.java
@@ -0,0 +1,171 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.JsonFileType;
+import com.intellij.json.JsonLexer;
+import com.intellij.json.json5.Json5FileType;
+import com.intellij.json.json5.Json5Lexer;
+import com.intellij.lexer.Lexer;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.project.DumbService;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.TokenType;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.indexing.*;
+import com.intellij.util.io.DataExternalizer;
+import com.intellij.util.io.EnumeratorStringDescriptor;
+import com.intellij.util.io.KeyDescriptor;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class JsonSchemaFileValuesIndex extends FileBasedIndexExtension<String, String> {
+ public static final ID<String, String> INDEX_ID = ID.create("json.file.root.values");
+ private static final int VERSION = 5;
+ public static final String NULL = "$NULL$";
+
+ @NotNull
+ @Override
+ public ID<String, String> getName() {
+ return INDEX_ID;
+ }
+
+ private final DataIndexer<String, String, FileContent> myIndexer =
+ new DataIndexer<String, String, FileContent>() {
+ @Override
+ @NotNull
+ public Map<String, String> map(@NotNull FileContent inputData) {
+ return readTopLevelProps(inputData.getFileType(), inputData.getContentAsText());
+ }
+ };
+
+ @NotNull
+ @Override
+ public DataIndexer<String, String, FileContent> getIndexer() {
+ return myIndexer;
+ }
+
+ @NotNull
+ @Override
+ public KeyDescriptor<String> getKeyDescriptor() {
+ return EnumeratorStringDescriptor.INSTANCE;
+ }
+
+ @NotNull
+ @Override
+ public DataExternalizer<String> getValueExternalizer() {
+ return EnumeratorStringDescriptor.INSTANCE;
+ }
+
+ @Override
+ public int getVersion() {
+ return VERSION;
+ }
+
+ @NotNull
+ @Override
+ public FileBasedIndex.InputFilter getInputFilter() {
+ return file -> file.getFileType() instanceof JsonFileType;
+ }
+
+ @Override
+ public boolean dependsOnFileContent() {
+ return true;
+ }
+
+ @Nullable
+ public static String getCachedValue(Project project, VirtualFile file, String requestedKey) {
+ if (project.isDisposed() || !file.isValid() || DumbService.isDumb(project)) return NULL;
+ List<String> values = FileBasedIndex.getInstance().getValues(INDEX_ID, requestedKey, GlobalSearchScope.fileScope(project, file));
+ if (values.size() == 1) {
+ return values.get(0);
+ }
+
+ return null;
+ }
+
+ @NotNull
+ static Map<String, String> readTopLevelProps(@NotNull FileType fileType, @NotNull CharSequence content) {
+ if (!(fileType instanceof JsonFileType)) return ContainerUtil.newHashMap();
+
+ Lexer lexer = fileType == Json5FileType.INSTANCE ? new Json5Lexer() : new JsonLexer();
+ final HashMap<String, String> map = ContainerUtil.newHashMap();
+ lexer.start(content);
+
+ // We only care about properties at the root level having the form of "property" : "value".
+ int nesting = 0;
+ boolean idFound = false;
+ boolean obsoleteIdFound = false;
+ boolean schemaFound = false;
+ while (!(idFound && schemaFound && obsoleteIdFound) && lexer.getCurrentPosition().getOffset() < lexer.getBufferEnd()) {
+ IElementType token = lexer.getTokenType();
+ // Nesting level can only change at curly braces.
+ if (token == JsonElementTypes.L_CURLY) {
+ nesting++;
+ }
+ else if (token == JsonElementTypes.R_CURLY) {
+ nesting--;
+ }
+ else if (nesting == 1 &&
+ (token == JsonElementTypes.DOUBLE_QUOTED_STRING
+ || token == JsonElementTypes.SINGLE_QUOTED_STRING
+ || token == JsonElementTypes.IDENTIFIER)) {
+ // We are looking for two special properties at the root level.
+ switch (lexer.getTokenText()) {
+ case "$id":
+ case "\"$id\"":
+ case "'$id'":
+ idFound |= captureValueIfString(lexer, map, JsonCachedValues.ID_CACHE_KEY);
+ break;
+ case "id":
+ case "\"id\"":
+ case "'id'":
+ obsoleteIdFound |= captureValueIfString(lexer, map, JsonCachedValues.OBSOLETE_ID_CACHE_KEY);
+ break;
+ case "$schema":
+ case "\"$schema\"":
+ case "'$schema'":
+ schemaFound |= captureValueIfString(lexer, map, JsonCachedValues.URL_CACHE_KEY);
+ break;
+ }
+ }
+ lexer.advance();
+ }
+ if (!map.containsKey(JsonCachedValues.ID_CACHE_KEY)) map.put(JsonCachedValues.ID_CACHE_KEY, NULL);
+ if (!map.containsKey(JsonCachedValues.OBSOLETE_ID_CACHE_KEY)) map.put(JsonCachedValues.OBSOLETE_ID_CACHE_KEY, NULL);
+ if (!map.containsKey(JsonCachedValues.URL_CACHE_KEY)) map.put(JsonCachedValues.URL_CACHE_KEY, NULL);
+ return map;
+ }
+
+ private static boolean captureValueIfString(@NotNull Lexer lexer, @NotNull HashMap<String, String> destMap, @NotNull String key) {
+ IElementType token;
+ lexer.advance();
+ token = skipWhitespacesAndGetTokenType(lexer);
+ if (token == JsonElementTypes.COLON) {
+ lexer.advance();
+ token = skipWhitespacesAndGetTokenType(lexer);
+ if (token == JsonElementTypes.DOUBLE_QUOTED_STRING || token == JsonElementTypes.SINGLE_QUOTED_STRING) {
+ destMap.put(key, lexer.getTokenText().substring(1, lexer.getTokenText().length() - 1));
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ private static IElementType skipWhitespacesAndGetTokenType(@NotNull Lexer lexer) {
+ while (lexer.getTokenType() == TokenType.WHITE_SPACE ||
+ lexer.getTokenType() == JsonElementTypes.LINE_COMMENT ||
+ lexer.getTokenType() == JsonElementTypes.BLOCK_COMMENT) {
+ lexer.advance();
+ }
+ return lexer.getTokenType();
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaGotoDeclarationHandler.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaGotoDeclarationHandler.java
new file mode 100644
index 00000000..072bd5f5
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaGotoDeclarationHandler.java
@@ -0,0 +1,48 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler;
+import com.intellij.json.JsonElementTypes;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.psi.util.PsiUtilCore;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class JsonSchemaGotoDeclarationHandler implements GotoDeclarationHandler {
+ @Nullable
+ @Override
+ public PsiElement[] getGotoDeclarationTargets(@Nullable PsiElement sourceElement, int offset, Editor editor) {
+ final IElementType elementType = PsiUtilCore.getElementType(sourceElement);
+ if (elementType != JsonElementTypes.DOUBLE_QUOTED_STRING && elementType != JsonElementTypes.SINGLE_QUOTED_STRING) return null;
+ final JsonStringLiteral literal = PsiTreeUtil.getParentOfType(sourceElement, JsonStringLiteral.class);
+ if (literal == null) return null;
+ final PsiElement parent = literal.getParent();
+ if (parent instanceof JsonProperty && ((JsonProperty)parent).getNameElement() == literal) {
+ final JsonSchemaService service = JsonSchemaService.Impl.get(literal.getProject());
+ final PsiFile containingFile = literal.getContainingFile();
+ final VirtualFile file = containingFile.getVirtualFile();
+ if (file == null || !service.isApplicableToFile(file)) return null;
+ final List<JsonSchemaVariantsTreeBuilder.Step> steps = JsonOriginalPsiWalker.INSTANCE.findPosition(literal, true);
+ if (steps == null) return null;
+ final JsonSchemaObject schemaObject = service.getSchemaObject(file);
+ if (schemaObject != null) {
+ final PsiElement target = new JsonSchemaResolver(schemaObject, false, steps)
+ .findNavigationTarget(false, ((JsonProperty)parent).getValue(),
+ JsonSchemaService.isSchemaFile(containingFile));
+ if (target != null) {
+ return new PsiElement[] {target};
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaInJsonFilesEnabler.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaInJsonFilesEnabler.java
new file mode 100644
index 00000000..998542b6
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaInJsonFilesEnabler.java
@@ -0,0 +1,13 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.JsonUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.jsonSchema.extension.JsonSchemaEnabler;
+
+public class JsonSchemaInJsonFilesEnabler implements JsonSchemaEnabler {
+ @Override
+ public boolean isEnabledForFile(VirtualFile file) {
+ return JsonUtil.isJsonFile(file);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java
new file mode 100644
index 00000000..8976179e
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java
@@ -0,0 +1,1180 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.intellij.json.psi.JsonContainer;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.progress.ProcessCanceledException;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.containers.ContainerUtilRt;
+import com.jetbrains.jsonSchema.JsonSchemaVfsListener;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.remote.JsonFileResolver;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.jetbrains.jsonSchema.JsonPointerUtil.*;
+
+/**
+ * @author Irina.Chernushina on 8/28/2015.
+ */
+public class JsonSchemaObject {
+ private static final Logger LOG = Logger.getInstance(JsonSchemaObject.class);
+
+ @NonNls public static final String DEFINITIONS = "definitions";
+ @NonNls public static final String PROPERTIES = "properties";
+ @NonNls public static final String ITEMS = "items";
+ @NonNls public static final String ADDITIONAL_ITEMS = "additionalItems";
+ @NonNls public static final String X_INTELLIJ_HTML_DESCRIPTION = "x-intellij-html-description";
+ @Nullable private final JsonContainer myJsonObject;
+ @Nullable private Map<String, JsonSchemaObject> myDefinitionsMap;
+ @NotNull private static final JsonSchemaObject NULL_OBJ = new JsonSchemaObject();
+ @NotNull private final ConcurrentMap<String, JsonSchemaObject> myComputedRefs = new ConcurrentHashMap<>();
+ @NotNull private final AtomicBoolean mySubscribed = new AtomicBoolean(false);
+ @NotNull private Map<String, JsonSchemaObject> myProperties;
+
+ @Nullable private PatternProperties myPatternProperties;
+ @Nullable private PropertyNamePattern myPattern;
+
+ @Nullable private String myId;
+ @Nullable private String mySchema;
+
+ @Nullable private String myTitle;
+ @Nullable private String myDescription;
+ @Nullable private String myHtmlDescription;
+
+ @Nullable private JsonSchemaType myType;
+ @Nullable private Object myDefault;
+ @Nullable private String myRef;
+ @Nullable private String myFormat;
+ @Nullable private Set<JsonSchemaType> myTypeVariants;
+ @Nullable private Number myMultipleOf;
+ @Nullable private Number myMaximum;
+ private boolean myExclusiveMaximum;
+ @Nullable private Number myExclusiveMaximumNumber;
+ @Nullable private Number myMinimum;
+ private boolean myExclusiveMinimum;
+ @Nullable private Number myExclusiveMinimumNumber;
+ @Nullable private Integer myMaxLength;
+ @Nullable private Integer myMinLength;
+
+ @Nullable private Boolean myAdditionalPropertiesAllowed;
+ @Nullable private JsonSchemaObject myAdditionalPropertiesSchema;
+ @Nullable private JsonSchemaObject myPropertyNamesSchema;
+
+ @Nullable private Boolean myAdditionalItemsAllowed;
+ @Nullable private JsonSchemaObject myAdditionalItemsSchema;
+
+ @Nullable private JsonSchemaObject myItemsSchema;
+ @Nullable private JsonSchemaObject myContainsSchema;
+ @Nullable private List<JsonSchemaObject> myItemsSchemaList;
+
+ @Nullable private Integer myMaxItems;
+ @Nullable private Integer myMinItems;
+
+ @Nullable private Boolean myUniqueItems;
+
+ @Nullable private Integer myMaxProperties;
+ @Nullable private Integer myMinProperties;
+ @Nullable private Set<String> myRequired;
+
+ @Nullable private Map<String, List<String>> myPropertyDependencies;
+ @Nullable private Map<String, JsonSchemaObject> mySchemaDependencies;
+
+ @Nullable private List<Object> myEnum;
+
+ @Nullable private List<JsonSchemaObject> myAllOf;
+ @Nullable private List<JsonSchemaObject> myAnyOf;
+ @Nullable private List<JsonSchemaObject> myOneOf;
+ @Nullable private JsonSchemaObject myNot;
+ @Nullable private JsonSchemaObject myIf;
+ @Nullable private JsonSchemaObject myThen;
+ @Nullable private JsonSchemaObject myElse;
+ private boolean myShouldValidateAgainstJSType;
+
+ public boolean isValidByExclusion() {
+ return myIsValidByExclusion;
+ }
+
+ private boolean myIsValidByExclusion = true;
+
+ public JsonSchemaObject(@NotNull JsonContainer object) {
+ myJsonObject = object;
+ myProperties = new HashMap<>();
+ }
+
+ private JsonSchemaObject() {
+ myJsonObject = null;
+ myProperties = new HashMap<>();
+ }
+
+ @Nullable
+ private static JsonSchemaType getSubtypeOfBoth(@NotNull JsonSchemaType selfType,
+ @NotNull JsonSchemaType otherType) {
+ if (otherType == JsonSchemaType._any) return selfType;
+ if (selfType == JsonSchemaType._any) return otherType;
+ switch (selfType) {
+ case _string:
+ return otherType == JsonSchemaType._string || otherType == JsonSchemaType._string_number ? JsonSchemaType._string : null;
+ case _number:
+ if (otherType == JsonSchemaType._integer) return JsonSchemaType._integer;
+ return otherType == JsonSchemaType._number || otherType == JsonSchemaType._string_number ? JsonSchemaType._number : null;
+ case _integer:
+ return otherType == JsonSchemaType._number
+ || otherType == JsonSchemaType._string_number
+ || otherType == JsonSchemaType._integer ? JsonSchemaType._integer : null;
+ case _object:
+ return otherType == JsonSchemaType._object ? JsonSchemaType._object : null;
+ case _array:
+ return otherType == JsonSchemaType._array ? JsonSchemaType._array : null;
+ case _boolean:
+ return otherType == JsonSchemaType._boolean ? JsonSchemaType._boolean : null;
+ case _null:
+ return otherType == JsonSchemaType._null ? JsonSchemaType._null : null;
+ case _string_number:
+ return otherType == JsonSchemaType._integer
+ || otherType == JsonSchemaType._number
+ || otherType == JsonSchemaType._string
+ || otherType == JsonSchemaType._string_number ? otherType : null;
+ }
+ return otherType;
+ }
+
+ @Nullable
+ private JsonSchemaType mergeTypes(@Nullable JsonSchemaType selfType,
+ @Nullable JsonSchemaType otherType,
+ @Nullable Set<JsonSchemaType> otherTypeVariants) {
+ if (selfType == null) return otherType;
+ if (otherType == null) {
+ if (otherTypeVariants != null && !otherTypeVariants.isEmpty()) {
+ Set<JsonSchemaType> filteredVariants = ContainerUtil.newHashSet(otherTypeVariants.size());
+ for (JsonSchemaType variant : otherTypeVariants) {
+ JsonSchemaType subtype = getSubtypeOfBoth(selfType, variant);
+ if (subtype != null) filteredVariants.add(subtype);
+ }
+ if (filteredVariants.size() == 0) {
+ myIsValidByExclusion = false;
+ return selfType;
+ }
+ if (filteredVariants.size() == 1) {
+ return filteredVariants.iterator().next();
+ }
+ return null; // will be handled by variants
+ }
+ return selfType;
+ }
+
+ JsonSchemaType subtypeOfBoth = getSubtypeOfBoth(selfType, otherType);
+ if (subtypeOfBoth == null){
+ myIsValidByExclusion = false;
+ return otherType;
+ }
+ return subtypeOfBoth;
+ }
+
+ private Set<JsonSchemaType> mergeTypeVariantSets(@Nullable Set<JsonSchemaType> self, @Nullable Set<JsonSchemaType> other) {
+ if (self == null) return other;
+ if (other == null) return self;
+
+ Set<JsonSchemaType> resultSet = ContainerUtil.newHashSet(self.size());
+ for (JsonSchemaType type : self) {
+ JsonSchemaType merged = mergeTypes(type, null, other);
+ if (merged != null) resultSet.add(merged);
+ }
+
+ if (resultSet.isEmpty()) {
+ myIsValidByExclusion = false;
+ return other;
+ }
+
+ return resultSet;
+ }
+
+ // peer pointer is not merged!
+ public void mergeValues(@NotNull JsonSchemaObject other) {
+ // we do not copy id, schema
+ mergeProperties(this, other);
+ myDefinitionsMap = copyMap(myDefinitionsMap, other.myDefinitionsMap);
+ final Map<String, JsonSchemaObject> map = copyMap(myPatternProperties == null ? null : myPatternProperties.mySchemasMap,
+ other.myPatternProperties == null ? null : other.myPatternProperties.mySchemasMap);
+ myPatternProperties = map == null ? null : new PatternProperties(map);
+
+ if (!StringUtil.isEmptyOrSpaces(other.myTitle)) {
+ myTitle = other.myTitle;
+ }
+ if (!StringUtil.isEmptyOrSpaces(other.myDescription)) {
+ myDescription = other.myDescription;
+ }
+ if (!StringUtil.isEmptyOrSpaces(other.myHtmlDescription)) {
+ myHtmlDescription = other.myHtmlDescription;
+ }
+
+ myType = mergeTypes(myType, other.myType, other.myTypeVariants);
+
+ if (other.myDefault != null) myDefault = other.myDefault;
+ if (other.myRef != null) myRef = other.myRef;
+ if (other.myFormat != null) myFormat = other.myFormat;
+ myTypeVariants = mergeTypeVariantSets(myTypeVariants, other.myTypeVariants);
+ if (other.myMultipleOf != null) myMultipleOf = other.myMultipleOf;
+ if (other.myMaximum != null) myMaximum = other.myMaximum;
+ if (other.myExclusiveMaximumNumber != null) myExclusiveMaximumNumber = other.myExclusiveMaximumNumber;
+ myExclusiveMaximum |= other.myExclusiveMaximum;
+ if (other.myMinimum != null) myMinimum = other.myMinimum;
+ if (other.myExclusiveMinimumNumber != null) myExclusiveMinimumNumber = other.myExclusiveMinimumNumber;
+ myExclusiveMinimum |= other.myExclusiveMinimum;
+ if (other.myMaxLength != null) myMaxLength = other.myMaxLength;
+ if (other.myMinLength != null) myMinLength = other.myMinLength;
+ if (other.myPattern != null) myPattern = other.myPattern;
+ if (other.myAdditionalPropertiesAllowed != null) myAdditionalPropertiesAllowed = other.myAdditionalPropertiesAllowed;
+ if (other.myAdditionalPropertiesSchema != null) myAdditionalPropertiesSchema = other.myAdditionalPropertiesSchema;
+ if (other.myPropertyNamesSchema != null) myPropertyNamesSchema = other.myPropertyNamesSchema;
+ if (other.myAdditionalItemsAllowed != null) myAdditionalItemsAllowed = other.myAdditionalItemsAllowed;
+ if (other.myAdditionalItemsSchema != null) myAdditionalItemsSchema = other.myAdditionalItemsSchema;
+ if (other.myItemsSchema != null) myItemsSchema = other.myItemsSchema;
+ if (other.myContainsSchema != null) myContainsSchema = other.myContainsSchema;
+ myItemsSchemaList = copyList(myItemsSchemaList, other.myItemsSchemaList);
+ if (other.myMaxItems != null) myMaxItems = other.myMaxItems;
+ if (other.myMinItems != null) myMinItems = other.myMinItems;
+ if (other.myUniqueItems != null) myUniqueItems = other.myUniqueItems;
+ if (other.myMaxProperties != null) myMaxProperties = other.myMaxProperties;
+ if (other.myMinProperties != null) myMinProperties = other.myMinProperties;
+ if (myRequired != null && other.myRequired != null) {
+ myRequired.addAll(other.myRequired);
+ }
+ else if (other.myRequired != null) {
+ myRequired = other.myRequired;
+ }
+ myPropertyDependencies = copyMap(myPropertyDependencies, other.myPropertyDependencies);
+ mySchemaDependencies = copyMap(mySchemaDependencies, other.mySchemaDependencies);
+ if (other.myEnum != null) myEnum = other.myEnum;
+ myAllOf = copyList(myAllOf, other.myAllOf);
+ myAnyOf = copyList(myAnyOf, other.myAnyOf);
+ myOneOf = copyList(myOneOf, other.myOneOf);
+ if (other.myNot != null) myNot = other.myNot;
+ if (other.myIf != null) myIf = other.myIf;
+ if (other.myThen != null) myThen = other.myThen;
+ if (other.myElse != null) myElse = other.myElse;
+ myShouldValidateAgainstJSType |= other.myShouldValidateAgainstJSType;
+ }
+
+ private static void mergeProperties(@NotNull JsonSchemaObject thisObject, @NotNull JsonSchemaObject otherObject) {
+ for (Map.Entry<String, JsonSchemaObject> prop: otherObject.myProperties.entrySet()) {
+ String key = prop.getKey();
+ JsonSchemaObject otherProp = prop.getValue();
+ if (!thisObject.myProperties.containsKey(key)) {
+ thisObject.myProperties.put(key, otherProp);
+ }
+ else {
+ JsonSchemaObject existingProp = thisObject.myProperties.get(key);
+ thisObject.myProperties.put(key, JsonSchemaVariantsTreeBuilder.merge(existingProp, otherProp, otherProp));
+ }
+ }
+ }
+
+ public void shouldValidateAgainstJSType() {
+ myShouldValidateAgainstJSType = true;
+ }
+
+ public boolean isShouldValidateAgainstJSType() {
+ return myShouldValidateAgainstJSType;
+ }
+
+ @Nullable
+ private static <T> List<T> copyList(@Nullable List<T> target, @Nullable List<T> source) {
+ if (source == null || source.isEmpty()) return target;
+ if (target == null) target = ContainerUtil.newArrayListWithCapacity(source.size());
+ target.addAll(source);
+ return target;
+ }
+
+ @Nullable
+ private static <K, V> Map<K, V> copyMap(@Nullable Map<K, V> target, @Nullable Map<K, V> source) {
+ if (source == null || source.isEmpty()) return target;
+ if (target == null) target = ContainerUtilRt.newHashMap(source.size());
+ target.putAll(source);
+ return target;
+ }
+
+ @NotNull
+ public VirtualFile getSchemaFile() {
+ assert myJsonObject != null;
+ return myJsonObject.getContainingFile().getViewProvider().getVirtualFile();
+ }
+
+ @NotNull
+ public JsonContainer getJsonObject() {
+ assert myJsonObject != null;
+ return myJsonObject;
+ }
+
+ @Nullable
+ public Map<String, JsonSchemaObject> getDefinitionsMap() {
+ return myDefinitionsMap;
+ }
+
+ public void setDefinitionsMap(@NotNull Map<String, JsonSchemaObject> definitionsMap) {
+ myDefinitionsMap = definitionsMap;
+ }
+
+ @NotNull
+ public Map<String, JsonSchemaObject> getProperties() {
+ return myProperties;
+ }
+
+ public void setProperties(@NotNull Map<String, JsonSchemaObject> properties) {
+ myProperties = properties;
+ }
+
+ public boolean hasPatternProperties() {
+ return myPatternProperties != null;
+ }
+
+ public void setPatternProperties(@NotNull Map<String, JsonSchemaObject> patternProperties) {
+ myPatternProperties = new PatternProperties(patternProperties);
+ }
+
+ @Nullable
+ public JsonSchemaType getType() {
+ return myType;
+ }
+
+ public void setType(@Nullable JsonSchemaType type) {
+ myType = type;
+ }
+
+ @Nullable
+ public Number getMultipleOf() {
+ return myMultipleOf;
+ }
+
+ public void setMultipleOf(@Nullable Number multipleOf) {
+ myMultipleOf = multipleOf;
+ }
+
+ @Nullable
+ public Number getMaximum() {
+ return myMaximum;
+ }
+
+ public void setMaximum(@Nullable Number maximum) {
+ myMaximum = maximum;
+ }
+
+ public boolean isExclusiveMaximum() {
+ return myExclusiveMaximum;
+ }
+
+ @Nullable
+ public Number getExclusiveMaximumNumber() {
+ return myExclusiveMaximumNumber;
+ }
+
+ public void setExclusiveMaximumNumber(@Nullable Number exclusiveMaximumNumber) {
+ myExclusiveMaximumNumber = exclusiveMaximumNumber;
+ }
+
+ @Nullable
+ public Number getExclusiveMinimumNumber() {
+ return myExclusiveMinimumNumber;
+ }
+
+ public void setExclusiveMinimumNumber(@Nullable Number exclusiveMinimumNumber) {
+ myExclusiveMinimumNumber = exclusiveMinimumNumber;
+ }
+
+ public void setExclusiveMaximum(boolean exclusiveMaximum) {
+ myExclusiveMaximum = exclusiveMaximum;
+ }
+
+ @Nullable
+ public Number getMinimum() {
+ return myMinimum;
+ }
+
+ public void setMinimum(@Nullable Number minimum) {
+ myMinimum = minimum;
+ }
+
+ public boolean isExclusiveMinimum() {
+ return myExclusiveMinimum;
+ }
+
+ public void setExclusiveMinimum(boolean exclusiveMinimum) {
+ myExclusiveMinimum = exclusiveMinimum;
+ }
+
+ @Nullable
+ public Integer getMaxLength() {
+ return myMaxLength;
+ }
+
+ public void setMaxLength(@Nullable Integer maxLength) {
+ myMaxLength = maxLength;
+ }
+
+ @Nullable
+ public Integer getMinLength() {
+ return myMinLength;
+ }
+
+ public void setMinLength(@Nullable Integer minLength) {
+ myMinLength = minLength;
+ }
+
+ @Nullable
+ public String getPattern() {
+ return myPattern == null ? null : myPattern.getPattern();
+ }
+
+ public void setPattern(@Nullable String pattern) {
+ myPattern = pattern == null ? null : new PropertyNamePattern(pattern);
+ }
+
+ @Nullable
+ public Boolean getAdditionalPropertiesAllowed() {
+ return myAdditionalPropertiesAllowed == null || myAdditionalPropertiesAllowed;
+ }
+
+ public void setAdditionalPropertiesAllowed(@Nullable Boolean additionalPropertiesAllowed) {
+ myAdditionalPropertiesAllowed = additionalPropertiesAllowed;
+ }
+
+ @Nullable
+ public JsonSchemaObject getPropertyNamesSchema() {
+ return myPropertyNamesSchema;
+ }
+
+ public void setPropertyNamesSchema(@Nullable JsonSchemaObject propertyNamesSchema) {
+ myPropertyNamesSchema = propertyNamesSchema;
+ }
+
+ @Nullable
+ public JsonSchemaObject getAdditionalPropertiesSchema() {
+ return myAdditionalPropertiesSchema;
+ }
+
+ public void setAdditionalPropertiesSchema(@Nullable JsonSchemaObject additionalPropertiesSchema) {
+ myAdditionalPropertiesSchema = additionalPropertiesSchema;
+ }
+
+ @Nullable
+ public Boolean getAdditionalItemsAllowed() {
+ return myAdditionalItemsAllowed == null || myAdditionalItemsAllowed;
+ }
+
+ public void setAdditionalItemsAllowed(@Nullable Boolean additionalItemsAllowed) {
+ myAdditionalItemsAllowed = additionalItemsAllowed;
+ }
+
+ @Nullable
+ public JsonSchemaObject getAdditionalItemsSchema() {
+ return myAdditionalItemsSchema;
+ }
+
+ public void setAdditionalItemsSchema(@Nullable JsonSchemaObject additionalItemsSchema) {
+ myAdditionalItemsSchema = additionalItemsSchema;
+ }
+
+ @Nullable
+ public JsonSchemaObject getItemsSchema() {
+ return myItemsSchema;
+ }
+
+ public void setItemsSchema(@Nullable JsonSchemaObject itemsSchema) {
+ myItemsSchema = itemsSchema;
+ }
+
+ @Nullable
+ public JsonSchemaObject getContainsSchema() {
+ return myContainsSchema;
+ }
+
+ public void setContainsSchema(@Nullable JsonSchemaObject containsSchema) {
+ myContainsSchema = containsSchema;
+ }
+
+ @Nullable
+ public List<JsonSchemaObject> getItemsSchemaList() {
+ return myItemsSchemaList;
+ }
+
+ public void setItemsSchemaList(@Nullable List<JsonSchemaObject> itemsSchemaList) {
+ myItemsSchemaList = itemsSchemaList;
+ }
+
+ @Nullable
+ public Integer getMaxItems() {
+ return myMaxItems;
+ }
+
+ public void setMaxItems(@Nullable Integer maxItems) {
+ myMaxItems = maxItems;
+ }
+
+ @Nullable
+ public Integer getMinItems() {
+ return myMinItems;
+ }
+
+ public void setMinItems(@Nullable Integer minItems) {
+ myMinItems = minItems;
+ }
+
+ public boolean isUniqueItems() {
+ return Boolean.TRUE.equals(myUniqueItems);
+ }
+
+ public void setUniqueItems(boolean uniqueItems) {
+ myUniqueItems = uniqueItems;
+ }
+
+ @Nullable
+ public Integer getMaxProperties() {
+ return myMaxProperties;
+ }
+
+ public void setMaxProperties(@Nullable Integer maxProperties) {
+ myMaxProperties = maxProperties;
+ }
+
+ @Nullable
+ public Integer getMinProperties() {
+ return myMinProperties;
+ }
+
+ public void setMinProperties(@Nullable Integer minProperties) {
+ myMinProperties = minProperties;
+ }
+
+ @Nullable
+ public Set<String> getRequired() {
+ return myRequired;
+ }
+
+ public void setRequired(@Nullable Set<String> required) {
+ myRequired = required;
+ }
+
+ @Nullable
+ public Map<String, List<String>> getPropertyDependencies() {
+ return myPropertyDependencies;
+ }
+
+ public void setPropertyDependencies(@Nullable Map<String, List<String>> propertyDependencies) {
+ myPropertyDependencies = propertyDependencies;
+ }
+
+ @Nullable
+ public Map<String, JsonSchemaObject> getSchemaDependencies() {
+ return mySchemaDependencies;
+ }
+
+ public void setSchemaDependencies(@Nullable Map<String, JsonSchemaObject> schemaDependencies) {
+ mySchemaDependencies = schemaDependencies;
+ }
+
+ @Nullable
+ public List<Object> getEnum() {
+ return myEnum;
+ }
+
+ public void setEnum(@Nullable List<Object> anEnum) {
+ myEnum = anEnum;
+ }
+
+ @Nullable
+ public List<JsonSchemaObject> getAllOf() {
+ return myAllOf;
+ }
+
+ public void setAllOf(@Nullable List<JsonSchemaObject> allOf) {
+ myAllOf = allOf;
+ }
+
+ @Nullable
+ public List<JsonSchemaObject> getAnyOf() {
+ return myAnyOf;
+ }
+
+ public void setAnyOf(@Nullable List<JsonSchemaObject> anyOf) {
+ myAnyOf = anyOf;
+ }
+
+ @Nullable
+ public List<JsonSchemaObject> getOneOf() {
+ return myOneOf;
+ }
+
+ public void setOneOf(@Nullable List<JsonSchemaObject> oneOf) {
+ myOneOf = oneOf;
+ }
+
+ @Nullable
+ public JsonSchemaObject getNot() {
+ return myNot;
+ }
+
+ public void setNot(@Nullable JsonSchemaObject not) {
+ myNot = not;
+ }
+
+ @Nullable
+ public JsonSchemaObject getIf() {
+ return myIf;
+ }
+
+ public void setIf(@Nullable JsonSchemaObject anIf) {
+ myIf = anIf;
+ }
+
+ @Nullable
+ public JsonSchemaObject getThen() {
+ return myThen;
+ }
+
+ public void setThen(@Nullable JsonSchemaObject then) {
+ myThen = then;
+ }
+
+ @Nullable
+ public JsonSchemaObject getElse() {
+ return myElse;
+ }
+
+ public void setElse(@Nullable JsonSchemaObject anElse) {
+ myElse = anElse;
+ }
+
+ @Nullable
+ public Set<JsonSchemaType> getTypeVariants() {
+ return myTypeVariants;
+ }
+
+ public void setTypeVariants(@Nullable Set<JsonSchemaType> typeVariants) {
+ myTypeVariants = typeVariants;
+ }
+
+ @Nullable
+ public String getRef() {
+ return myRef;
+ }
+
+ public void setRef(@Nullable String ref) {
+ myRef = ref;
+ }
+
+ @Nullable
+ public Object getDefault() {
+ if (JsonSchemaType._integer.equals(myType)) return myDefault instanceof Number ? ((Number)myDefault).intValue() : myDefault;
+ return myDefault;
+ }
+
+ public void setDefault(@Nullable Object aDefault) {
+ myDefault = aDefault;
+ }
+
+ @Nullable
+ public String getFormat() {
+ return myFormat;
+ }
+
+ public void setFormat(@Nullable String format) {
+ myFormat = format;
+ }
+
+ @Nullable
+ public String getId() {
+ return myId;
+ }
+
+ public void setId(@Nullable String id) {
+ myId = id;
+ }
+
+ @Nullable
+ public String getSchema() {
+ return mySchema;
+ }
+
+ public void setSchema(@Nullable String schema) {
+ mySchema = schema;
+ }
+
+ @Nullable
+ public String getDescription() {
+ return myDescription;
+ }
+
+ public void setDescription(@NotNull String description) {
+ myDescription = unescapeJsonString(description);
+ }
+
+ @Nullable
+ public String getHtmlDescription() {
+ return myHtmlDescription;
+ }
+
+ public void setHtmlDescription(@NotNull String htmlDescription) {
+ myHtmlDescription = unescapeJsonString(htmlDescription);
+ }
+
+ @Nullable
+ public String getTitle() {
+ return myTitle;
+ }
+
+ public void setTitle(@NotNull String title) {
+ myTitle = unescapeJsonString(title);
+ }
+
+ private static String unescapeJsonString(@NotNull final String text) {
+ try {
+ final String object = String.format("{\"prop\": \"%s\"}", text);
+ return new Gson().fromJson(object, JsonObject.class).get("prop").getAsString();
+ } catch (JsonParseException e) {
+ return text;
+ }
+ }
+
+ @Nullable
+ public JsonSchemaObject getMatchingPatternPropertySchema(@NotNull String name) {
+ if (myPatternProperties == null) return null;
+ return myPatternProperties.getPatternPropertySchema(name);
+ }
+
+ public boolean checkByPattern(@NotNull String value) {
+ return myPattern != null && myPattern.checkByPattern(value);
+ }
+
+ @Nullable
+ public String getPatternError() {
+ return myPattern == null ? null : myPattern.getPatternError();
+ }
+
+ @Nullable
+ public Map<JsonContainer, String> getInvalidPatternProperties() {
+ if (myPatternProperties != null) {
+ final Map<String, String> patterns = myPatternProperties.getInvalidPatterns();
+
+ return patterns.entrySet().stream().map(entry -> {
+ final JsonSchemaObject object = myPatternProperties.getSchemaForPattern(entry.getKey());
+ assert object != null;
+ return Pair.create(object.getJsonObject(), entry.getValue());
+ }).collect(Collectors.toMap(o -> o.getFirst(), o -> o.getSecond()));
+ }
+ return null;
+ }
+
+ @Nullable
+ public JsonSchemaObject findRelativeDefinition(@NotNull String ref) {
+ if (isSelfReference(ref)) {
+ return this;
+ }
+ if (!ref.startsWith("#/")) {
+ return null;
+ }
+ ref = ref.substring(2);
+ final List<String> parts = split(ref);
+ JsonSchemaObject current = this;
+ for (int i = 0; i < parts.size(); i++) {
+ if (current == null) return null;
+ final String part = parts.get(i);
+ if (DEFINITIONS.equals(part)) {
+ if (i == (parts.size() - 1)) return null;
+ //noinspection AssignmentToForLoopParameter
+ final String nextPart = parts.get(++i);
+ current = current.getDefinitionsMap() == null ? null : current.getDefinitionsMap().get(unescapeJsonPointerPart(nextPart));
+ continue;
+ }
+ if (PROPERTIES.equals(part)) {
+ if (i == (parts.size() - 1)) return null;
+ //noinspection AssignmentToForLoopParameter
+ current = current.getProperties().get(unescapeJsonPointerPart(parts.get(++i)));
+ continue;
+ }
+ if (ITEMS.equals(part)) {
+ if (i == (parts.size() - 1)) {
+ current = current.getItemsSchema();
+ }
+ else {
+ //noinspection AssignmentToForLoopParameter
+ Integer next = tryParseInt(parts.get(++i));
+ List<JsonSchemaObject> itemsSchemaList = current.getItemsSchemaList();
+ if (itemsSchemaList != null && next != null && next < itemsSchemaList.size()) {
+ current = itemsSchemaList.get(next);
+ }
+ }
+ continue;
+ }
+ if (ADDITIONAL_ITEMS.equals(part)) {
+ if (i == (parts.size() - 1)) {
+ current = current.getAdditionalItemsSchema();
+ }
+ continue;
+ }
+
+ current = current.getDefinitionsMap() == null ? null : current.getDefinitionsMap().get(part);
+ }
+ return current;
+ }
+
+ @Nullable
+ private static Integer tryParseInt(String s) {
+ try {
+ return Integer.parseInt(s);
+ }
+ catch (Exception __) {
+ return null;
+ }
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ JsonSchemaObject object = (JsonSchemaObject)o;
+
+ assert myJsonObject != null;
+ return myJsonObject.equals(object.myJsonObject);
+ }
+
+ @Override
+ public int hashCode() {
+ assert myJsonObject != null;
+ return myJsonObject.hashCode();
+ }
+
+ @NotNull
+ private static String adaptSchemaPattern(String pattern) {
+ pattern = pattern.startsWith("^") || pattern.startsWith("*") || pattern.startsWith(".") ? pattern : (".*" + pattern);
+ pattern = pattern.endsWith("+") || pattern.endsWith("*") || pattern.endsWith("$") ? pattern : (pattern + ".*");
+ pattern = pattern.replace("\\\\", "\\");
+ return pattern;
+ }
+
+
+ private static Pair<Pattern, String> compilePattern(@NotNull final String pattern) {
+ try {
+ return Pair.create(Pattern.compile(adaptSchemaPattern(pattern)), null);
+ } catch (PatternSyntaxException e) {
+ return Pair.create(null, e.getMessage());
+ }
+ }
+
+ public static boolean matchPattern(@NotNull final Pattern pattern, @NotNull final String s) {
+ try {
+ return pattern.matcher(StringUtil.newBombedCharSequence(s, 300)).matches();
+ } catch (ProcessCanceledException e) {
+ // something wrong with the pattern, infinite cycle?
+ Logger.getInstance(JsonSchemaObject.class).info("Pattern matching canceled");
+ return false;
+ } catch (Exception e) {
+ // catch exceptions around to prevent things like:
+ // https://bugs.openjdk.java.net/browse/JDK-6984178
+ Logger.getInstance(JsonSchemaObject.class).info(e);
+ return false;
+ }
+ }
+
+ @Nullable
+ public String getTypeDescription(boolean shortDesc) {
+ JsonSchemaType type = getType();
+ if (type != null) return type.getDescription();
+
+ Set<JsonSchemaType> possibleTypes = getTypeVariants();
+
+ String description = getTypesDescription(shortDesc, possibleTypes);
+ if (description != null) return description;
+
+ List<Object> anEnum = getEnum();
+ if (anEnum != null) {
+ return shortDesc ? "enum" : anEnum.stream().map(o -> o.toString()).collect(Collectors.joining(" | "));
+ }
+
+ JsonSchemaType guessedType = guessType();
+ if (guessedType != null) {
+ return guessedType.getDescription();
+ }
+
+ return null;
+ }
+
+ @Nullable
+ public JsonSchemaType guessType() {
+ // if we have an explicit type, here we are
+ JsonSchemaType type = getType();
+ if (type != null) return type;
+
+ // process type variants before heuristic type detection
+ final Set<JsonSchemaType> typeVariants = getTypeVariants();
+ if (typeVariants != null) {
+ final int size = typeVariants.size();
+ if (size == 1) {
+ return typeVariants.iterator().next();
+ }
+ else if (size >= 2) {
+ return null;
+ }
+ }
+
+ // heuristic type detection based on the set of applied constraints
+ boolean hasObjectChecks = hasObjectChecks();
+ boolean hasNumericChecks = hasNumericChecks();
+ boolean hasStringChecks = hasStringChecks();
+ boolean hasArrayChecks = hasArrayChecks();
+
+ if (hasObjectChecks && !hasNumericChecks && !hasStringChecks && !hasArrayChecks) {
+ return JsonSchemaType._object;
+ }
+ if (!hasObjectChecks && hasNumericChecks && !hasStringChecks && !hasArrayChecks) {
+ return JsonSchemaType._number;
+ }
+ if (!hasObjectChecks && !hasNumericChecks && hasStringChecks && !hasArrayChecks) {
+ return JsonSchemaType._string;
+ }
+ if (!hasObjectChecks && !hasNumericChecks && !hasStringChecks && hasArrayChecks) {
+ return JsonSchemaType._array;
+ }
+ return null;
+ }
+
+ public boolean hasNumericChecks() {
+ return getMultipleOf() != null
+ || getExclusiveMinimumNumber() != null
+ || getExclusiveMaximumNumber() != null
+ || getMaximum() != null
+ || getMinimum() != null;
+ }
+
+ public boolean hasStringChecks() {
+ return getPattern() != null || getFormat() != null;
+ }
+
+ public boolean hasArrayChecks() {
+ return isUniqueItems()
+ || getContainsSchema() != null
+ || getItemsSchema() != null
+ || getItemsSchemaList() != null
+ || getMinItems() != null
+ || getMaxItems() != null;
+ }
+
+ public boolean hasObjectChecks() {
+ return !getProperties().isEmpty()
+ || getPropertyNamesSchema() != null
+ || getPropertyDependencies() != null
+ || hasPatternProperties()
+ || getRequired() != null
+ || getMinProperties() != null
+ || getMaxProperties() != null;
+ }
+
+ @Nullable
+ static String getTypesDescription(boolean shortDesc, @Nullable Collection<JsonSchemaType> possibleTypes) {
+ if (possibleTypes == null || possibleTypes.size() == 0) return null;
+ if (possibleTypes.size() == 1) return possibleTypes.iterator().next().getDescription();
+ if (possibleTypes.contains(JsonSchemaType._any)) return JsonSchemaType._any.getDescription();
+
+ Stream<String> typeDescriptions = possibleTypes.stream().map(t -> t.getDescription()).distinct().sorted();
+ boolean isShort = false;
+ if (shortDesc) {
+ typeDescriptions = typeDescriptions.limit(3);
+ if (possibleTypes.size() > 3) isShort = true;
+ }
+ return typeDescriptions.collect(Collectors.joining(" | ", "", isShort ? "| ..." : ""));
+ }
+
+ @Nullable
+ public JsonSchemaObject resolveRefSchema(@NotNull JsonSchemaService service) {
+ final String ref = getRef();
+ assert !StringUtil.isEmptyOrSpaces(ref);
+ if (!myComputedRefs.containsKey(ref)){
+ JsonSchemaObject value = fetchSchemaFromRefDefinition(ref, this, service);
+ if (!mySubscribed.get()) {
+ getJsonObject().getProject().getMessageBus().connect().subscribe(JsonSchemaVfsListener.JSON_DEPS_CHANGED, () -> myComputedRefs.clear());
+ mySubscribed.set(true);
+ }
+ if (!JsonFileResolver.isHttpPath(ref)) {
+ service.registerReference(ref);
+ }
+ else if (value != null) {
+ // our aliases - if http ref actually refers to a local file with specific ID
+ PsiFile file = value.getJsonObject().getContainingFile();
+ if (file != null) {
+ VirtualFile virtualFile = file.getVirtualFile();
+ if (virtualFile != null && !(virtualFile instanceof HttpVirtualFile)) {
+ service.registerReference(virtualFile.getName());
+ }
+ }
+ }
+ myComputedRefs.put(ref, value == null ? NULL_OBJ : value);
+ }
+ JsonSchemaObject object = myComputedRefs.getOrDefault(ref, null);
+ return object == NULL_OBJ ? null : object;
+ }
+
+ @Nullable
+ private static JsonSchemaObject fetchSchemaFromRefDefinition(@NotNull String ref,
+ @NotNull final JsonSchemaObject schema,
+ @NotNull JsonSchemaService service) {
+
+ final VirtualFile schemaFile = schema.getSchemaFile();
+ final JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter splitter = new JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter(ref);
+ String schemaId = splitter.getSchemaId();
+ if (schemaId != null) {
+ final JsonSchemaObject refSchema = resolveSchemaByReference(service, schemaFile, schemaId);
+ if (refSchema == null) return null;
+ return findRelativeDefinition(refSchema, splitter);
+ }
+ final JsonSchemaObject rootSchema = service.getSchemaObjectForSchemaFile(schemaFile);
+ if (rootSchema == null) {
+ LOG.debug(String.format("Schema object not found for %s", schemaFile.getPath()));
+ return null;
+ }
+ return findRelativeDefinition(rootSchema, splitter);
+ }
+
+ @Nullable
+ private static JsonSchemaObject resolveSchemaByReference(@NotNull JsonSchemaService service,
+ @NotNull VirtualFile schemaFile,
+ @NotNull String schemaId) {
+ final VirtualFile refFile = service.findSchemaFileByReference(schemaId, schemaFile);
+ if (refFile == null) {
+ LOG.debug(String.format("Schema file not found by reference: '%s' from %s", schemaId, schemaFile.getPath()));
+ return null;
+ }
+ final JsonSchemaObject refSchema = service.getSchemaObjectForSchemaFile(refFile);
+ if (refSchema == null) {
+ LOG.debug(String.format("Schema object not found by reference: '%s' from %s", schemaId, schemaFile.getPath()));
+ return null;
+ }
+ return refSchema;
+ }
+
+ private static JsonSchemaObject findRelativeDefinition(@NotNull final JsonSchemaObject schema,
+ @NotNull final JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter splitter) {
+ final String path = splitter.getRelativePath();
+ if (StringUtil.isEmptyOrSpaces(path)) {
+ final String id = splitter.getSchemaId();
+ if (isSelfReference(id)) {
+ return schema;
+ }
+ if (id != null && id.startsWith("#")) {
+ final String resolvedId = JsonCachedValues.resolveId(schema.getJsonObject().getContainingFile(), id);
+ if (resolvedId == null || id.equals("#" + resolvedId)) return null;
+ return findRelativeDefinition(schema, new JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter("#" + resolvedId));
+ }
+ return schema;
+ }
+ final JsonSchemaObject definition = schema.findRelativeDefinition(path);
+ if (definition == null) {
+ LOG.debug(String.format("Definition not found by reference: '%s' in file %s", path, schema.getSchemaFile().getPath()));
+ }
+ return definition;
+ }
+
+ private static class PropertyNamePattern {
+ @NotNull private final String myPattern;
+ @Nullable private final Pattern myCompiledPattern;
+ @Nullable private final String myPatternError;
+ @NotNull private final Map<String, Boolean> myValuePatternCache;
+
+ PropertyNamePattern(@NotNull String pattern) {
+ myPattern = StringUtil.unescapeBackSlashes(pattern);
+ final Pair<Pattern, String> pair = compilePattern(pattern);
+ myPatternError = pair.getSecond();
+ myCompiledPattern = pair.getFirst();
+ myValuePatternCache = ContainerUtil.createConcurrentWeakKeyWeakValueMap();
+ }
+
+ @Nullable
+ public String getPatternError() {
+ return myPatternError;
+ }
+
+ boolean checkByPattern(@NotNull final String name) {
+ if (myPatternError != null) return true;
+ if (Boolean.TRUE.equals(myValuePatternCache.get(name))) return true;
+ assert myCompiledPattern != null;
+ boolean matches = matchPattern(myCompiledPattern, name);
+ myValuePatternCache.put(name, matches);
+ return matches;
+ }
+
+ @NotNull
+ public String getPattern() {
+ return myPattern;
+ }
+ }
+
+ private static class PatternProperties {
+ @NotNull private final Map<String, JsonSchemaObject> mySchemasMap;
+ @NotNull private final Map<String, Pattern> myCachedPatterns;
+ @NotNull private final Map<String, String> myCachedPatternProperties;
+ @NotNull private final Map<String, String> myInvalidPatterns;
+
+ PatternProperties(@NotNull final Map<String, JsonSchemaObject> schemasMap) {
+ mySchemasMap = new HashMap<>();
+ schemasMap.keySet().forEach(key -> mySchemasMap.put(StringUtil.unescapeBackSlashes(key), schemasMap.get(key)));
+ myCachedPatterns = new HashMap<>();
+ myCachedPatternProperties = ContainerUtil.createConcurrentWeakKeyWeakValueMap();
+ myInvalidPatterns = new HashMap<>();
+ mySchemasMap.keySet().forEach(key -> {
+ final Pair<Pattern, String> pair = compilePattern(key);
+ if (pair.getSecond() != null) {
+ myInvalidPatterns.put(key, pair.getSecond());
+ } else {
+ assert pair.getFirst() != null;
+ myCachedPatterns.put(key, pair.getFirst());
+ }
+ });
+ }
+
+ @Nullable
+ public JsonSchemaObject getPatternPropertySchema(@NotNull final String name) {
+ String value = myCachedPatternProperties.get(name);
+ if (value != null) {
+ assert mySchemasMap.containsKey(value);
+ return mySchemasMap.get(value);
+ }
+
+ value = myCachedPatterns.keySet().stream()
+ .filter(key -> matchPattern(myCachedPatterns.get(key), name))
+ .findFirst()
+ .orElse(null);
+ if (value != null) {
+ myCachedPatternProperties.put(name, value);
+ assert mySchemasMap.containsKey(value);
+ return mySchemasMap.get(value);
+ }
+ return null;
+ }
+
+ @NotNull
+ public Map<String, String> getInvalidPatterns() {
+ return myInvalidPatterns;
+ }
+
+ public JsonSchemaObject getSchemaForPattern(@NotNull String key) {
+ return mySchemasMap.get(key);
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java
new file mode 100644
index 00000000..c93c65c0
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java
@@ -0,0 +1,511 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.psi.*;
+import com.intellij.notification.NotificationGroup;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.io.FileUtilRt;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.PairConsumer;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 1/13/2017.
+ */
+public class JsonSchemaReader {
+ private static final int MAX_SCHEMA_LENGTH = FileUtilRt.MEGABYTE;
+ public static final Logger LOG = Logger.getInstance(JsonSchemaReader.class);
+ public static final NotificationGroup ERRORS_NOTIFICATION = NotificationGroup.logOnlyGroup("JSON Schema");
+
+ private final Map<String, JsonSchemaObject> myIds = new HashMap<>();
+ private final ArrayDeque<JsonSchemaObject> myQueue;
+
+ private static final Map<String, MyReader> READERS_MAP = new HashMap<>();
+ static {
+ fillMap();
+ }
+
+ public JsonSchemaReader() {
+ myQueue = new ArrayDeque<>();
+ }
+
+ @NotNull
+ public static JsonSchemaObject readFromFile(@NotNull Project project, @NotNull VirtualFile file) throws Exception {
+ if (!file.isValid()) {
+ throw new Exception(String.format("Can not load JSON Schema file '%s'", file.getName()));
+ }
+
+ final PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
+ if (!(psiFile instanceof JsonFile)) {
+ throw new Exception(String.format("Can not load code model for JSON Schema file '%s'", file.getName()));
+ }
+
+ final JsonObject value = ObjectUtils.tryCast(((JsonFile)psiFile).getTopLevelValue(), JsonObject.class);
+ if (value == null) {
+ throw new Exception(String.format("JSON Schema file '%s' must contain only one top-level object", file.getName()));
+ }
+ return new JsonSchemaReader().read(value);
+ }
+
+ @Nullable
+ public static String checkIfValidJsonSchema(@NotNull final Project project, @NotNull final VirtualFile file) {
+ final long length = file.getLength();
+ final String fileName = file.getName();
+ if (length > MAX_SCHEMA_LENGTH) {
+ return String.format("JSON schema was not loaded from '%s' because it's too large (file size is %d bytes).", fileName, length);
+ }
+ if (length == 0) {
+ return String.format("JSON schema was not loaded from '%s'. File is empty.", fileName);
+ }
+ try {
+ readFromFile(project, file);
+ } catch (Exception e) {
+ final String message = String.format("JSON Schema not found or contain error in '%s': %s", fileName, e.getMessage());
+ LOG.info(message);
+ return message;
+ }
+ return null;
+ }
+
+ public JsonSchemaObject read(@NotNull final JsonObject object) {
+ final JsonSchemaObject root = new JsonSchemaObject(object);
+ myQueue.add(root);
+ while (!myQueue.isEmpty()) {
+ final JsonSchemaObject currentSchema = myQueue.removeFirst();
+
+ final JsonContainer jsonObject = currentSchema.getJsonObject();
+ if (jsonObject instanceof JsonObject) {
+ final List<JsonProperty> list = ((JsonObject)jsonObject).getPropertyList();
+ for (JsonProperty property : list) {
+ if (property.getValue() == null) continue;
+ final MyReader reader = READERS_MAP.get(property.getName());
+ if (reader != null) {
+ reader.read(property.getValue(), currentSchema, myQueue);
+ }
+ else {
+ readSingleDefinition(property.getName(), property.getValue(), currentSchema);
+ }
+ }
+ }
+ else if (jsonObject instanceof JsonArray) {
+ List<JsonValue> values = ((JsonArray)jsonObject).getValueList();
+ for (int i = 0; i < values.size(); i++) {
+ readSingleDefinition(String.valueOf(i), values.get(i), currentSchema);
+ }
+ }
+
+ if (currentSchema.getId() != null) myIds.put(currentSchema.getId(), currentSchema);
+ }
+ return root;
+ }
+
+ public Map<String, JsonSchemaObject> getIds() {
+ return myIds;
+ }
+
+ private void readSingleDefinition(@NotNull String name, @NotNull JsonValue value, @NotNull JsonSchemaObject schema) {
+ if (value instanceof JsonContainer) {
+ final JsonSchemaObject defined = new JsonSchemaObject((JsonContainer)value);
+ myQueue.add(defined);
+ Map<String, JsonSchemaObject> definitions = schema.getDefinitionsMap();
+ if (definitions == null) schema.setDefinitionsMap(definitions = new HashMap<>());
+ definitions.put(name, defined);
+ }
+ }
+
+ private static void fillMap() {
+ READERS_MAP.put("$id", (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setId(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put("id", (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setId(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put("$schema", (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setSchema(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put("description", (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setDescription(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put(JsonSchemaObject.X_INTELLIJ_HTML_DESCRIPTION, (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setHtmlDescription(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put("title", (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setTitle(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put("$ref", (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setRef(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put("default", createDefault());
+ READERS_MAP.put("format", (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setFormat(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put(JsonSchemaObject.DEFINITIONS, createDefinitionsConsumer());
+ READERS_MAP.put(JsonSchemaObject.PROPERTIES, createPropertiesConsumer());
+ READERS_MAP.put("multipleOf", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMultipleOf(((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("maximum", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMaximum(((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("minimum", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMinimum(((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("exclusiveMaximum", (element, object, queue) -> {
+ if (element instanceof JsonBooleanLiteral) object.setExclusiveMaximum(((JsonBooleanLiteral)element).getValue());
+ if (element instanceof JsonNumberLiteral) object.setExclusiveMaximumNumber(((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("exclusiveMinimum", (element, object, queue) -> {
+ if (element instanceof JsonBooleanLiteral) object.setExclusiveMinimum(((JsonBooleanLiteral)element).getValue());
+ if (element instanceof JsonNumberLiteral) object.setExclusiveMinimumNumber(((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("maxLength", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMaxLength((int)((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("minLength", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMinLength((int)((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("pattern", (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) object.setPattern(StringUtil.unquoteString(element.getText()));
+ });
+ READERS_MAP.put(JsonSchemaObject.ADDITIONAL_ITEMS, createAdditionalItems());
+ READERS_MAP.put(JsonSchemaObject.ITEMS, createItems());
+ READERS_MAP.put("contains", createContains());
+ READERS_MAP.put("maxItems", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMaxItems((int)((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("minItems", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMinItems((int)((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("uniqueItems", (element, object, queue) -> {
+ if (element instanceof JsonBooleanLiteral) object.setUniqueItems(((JsonBooleanLiteral)element).getValue());
+ });
+ READERS_MAP.put("maxProperties", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMaxProperties((int)((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("minProperties", (element, object, queue) -> {
+ if (element instanceof JsonNumberLiteral) object.setMinProperties((int)((JsonNumberLiteral)element).getValue());
+ });
+ READERS_MAP.put("required", createRequired());
+ READERS_MAP.put("additionalProperties", createAdditionalProperties());
+ READERS_MAP.put("propertyNames", createPropertyNames());
+ READERS_MAP.put("patternProperties", createPatternProperties());
+ READERS_MAP.put("dependencies", createDependencies());
+ READERS_MAP.put("enum", createEnum());
+ READERS_MAP.put("const", (element, object, queue) -> {
+ if (element instanceof JsonValue) object.setEnum(ContainerUtil.createMaybeSingletonList(readEnumValue((JsonValue)element)));
+ });
+ READERS_MAP.put("type", createType());
+ READERS_MAP.put("allOf", createContainer((object, members) -> object.setAllOf(members)));
+ READERS_MAP.put("anyOf", createContainer((object, members) -> object.setAnyOf(members)));
+ READERS_MAP.put("oneOf", createContainer((object, members) -> object.setOneOf(members)));
+ READERS_MAP.put("not", createNot());
+ READERS_MAP.put("if", createIf());
+ READERS_MAP.put("then", createThen());
+ READERS_MAP.put("else", createElse());
+ READERS_MAP.put("instanceof", ((element, object, queue) -> object.shouldValidateAgainstJSType()));
+ READERS_MAP.put("typeof", ((element, object, queue) -> object.shouldValidateAgainstJSType()));
+ }
+
+ private static MyReader createIf() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonSchemaObject ifSchema = new JsonSchemaObject((JsonObject)element);
+ queue.add(ifSchema);
+ object.setIf(ifSchema);
+ }
+ };
+ }
+
+ private static MyReader createThen() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonSchemaObject ifSchema = new JsonSchemaObject((JsonObject)element);
+ queue.add(ifSchema);
+ object.setThen(ifSchema);
+ }
+ };
+ }
+
+ private static MyReader createElse() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonSchemaObject ifSchema = new JsonSchemaObject((JsonObject)element);
+ queue.add(ifSchema);
+ object.setElse(ifSchema);
+ }
+ };
+ }
+
+ private static MyReader createNot() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonSchemaObject not = new JsonSchemaObject((JsonObject)element);
+ queue.add(not);
+ object.setNot(not);
+ }
+ };
+ }
+
+ private static MyReader createContainer(@NotNull final PairConsumer<JsonSchemaObject, List<JsonSchemaObject>> delegate) {
+ return (element, object, queue) -> {
+ if (element instanceof JsonArray) {
+ final List<JsonValue> list = ((JsonArray)element).getValueList();
+ final List<JsonSchemaObject> members = list.stream().filter(el -> el instanceof JsonObject)
+ .map(el -> {
+ final JsonSchemaObject child = new JsonSchemaObject((JsonObject)el);
+ queue.add(child);
+ return child;
+ }).collect(Collectors.toList());
+ delegate.consume(object, members);
+ }
+ };
+ }
+
+ private static MyReader createType() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonStringLiteral) {
+ final JsonSchemaType type = parseType(StringUtil.unquoteString(element.getText()));
+ if (type != null) object.setType(type);
+ } else if (element instanceof JsonArray) {
+ final Set<JsonSchemaType> typeList = ((JsonArray)element).getValueList().stream()
+ .filter(notEmptyString()).map(el -> parseType(StringUtil.unquoteString(el.getText())))
+ .filter(el -> el != null).collect(Collectors.toSet());
+ if (!typeList.isEmpty()) object.setTypeVariants(typeList);
+ }
+ };
+ }
+
+ @Nullable
+ private static JsonSchemaType parseType(@NotNull final String typeString) {
+ try {
+ return JsonSchemaType.valueOf("_" + typeString);
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ @Nullable
+ private static Object readEnumValue(JsonValue value) {
+ if (value instanceof JsonStringLiteral) {
+ return "\"" + StringUtil.unquoteString(((JsonStringLiteral)value).getValue()) + "\"";
+ } else if (value instanceof JsonNumberLiteral) {
+ return getNumber((JsonNumberLiteral)value);
+ } else if (value instanceof JsonBooleanLiteral) {
+ return ((JsonBooleanLiteral)value).getValue();
+ } else if (value instanceof JsonNullLiteral) {
+ return "null";
+ } else if (value instanceof JsonArray) {
+ return new EnumArrayValueWrapper(((JsonArray)value).getValueList().stream().map(v -> readEnumValue(v)).filter(v -> v != null).toArray());
+ } else if (value instanceof JsonObject) {
+ return new EnumObjectValueWrapper(((JsonObject)value).getPropertyList().stream()
+ .map(p -> Pair.create(p.getName(), readEnumValue(p.getValue())))
+ .filter(p -> p.second != null)
+ .collect(Collectors.toMap(p -> p.first, p -> p.second)));
+ }
+ return null;
+ }
+
+ private static MyReader createEnum() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonArray) {
+ final List<Object> objects = new ArrayList<>();
+ final List<JsonValue> list = ((JsonArray)element).getValueList();
+ for (JsonValue value : list) {
+ Object enumValue = readEnumValue(value);
+ if (enumValue == null) return; // don't validate if we have unsupported entity kinds
+ objects.add(enumValue);
+ }
+ object.setEnum(objects);
+ }
+ };
+ }
+
+ @NotNull
+ private static Number getNumber(@NotNull JsonNumberLiteral value) {
+ Number numberValue;
+ try {
+ numberValue = Integer.parseInt(value.getText());
+ } catch (NumberFormatException e) {
+ numberValue = value.getValue();
+ }
+ return numberValue;
+ }
+
+ private static MyReader createDependencies() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final HashMap<String, List<String>> propertyDependencies = new HashMap<>();
+ final HashMap<String, JsonSchemaObject> schemaDependencies = new HashMap<>();
+
+ final List<JsonProperty> list = ((JsonObject)element).getPropertyList();
+ for (JsonProperty property : list) {
+ if (property.getValue() == null) continue;
+ if (property.getValue() instanceof JsonArray) {
+ final List<String> dependencies = ((JsonArray)property.getValue()).getValueList().stream()
+ .filter(notEmptyString())
+ .map(el -> StringUtil.unquoteString(el.getText())).collect(Collectors.toList());
+ if (!dependencies.isEmpty()) propertyDependencies.put(property.getName(), dependencies);
+ } else if (property.getValue() instanceof JsonObject) {
+ final JsonSchemaObject child = new JsonSchemaObject((JsonObject)property.getValue());
+ queue.add(child);
+ schemaDependencies.put(property.getName(), child);
+ }
+ }
+
+ object.setPropertyDependencies(propertyDependencies);
+ object.setSchemaDependencies(schemaDependencies);
+ }
+ };
+ }
+
+ @NotNull
+ private static Predicate<JsonValue> notEmptyString() {
+ return el -> el instanceof JsonStringLiteral && !StringUtil.isEmptyOrSpaces(el.getText());
+ }
+
+ private static MyReader createPatternProperties() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ object.setPatternProperties(readInnerObject((JsonObject)element, queue));
+ }
+ };
+ }
+
+ private static MyReader createAdditionalProperties() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonBooleanLiteral) {
+ object.setAdditionalPropertiesAllowed(((JsonBooleanLiteral)element).getValue());
+ } else if (element instanceof JsonObject) {
+ final JsonSchemaObject schema = new JsonSchemaObject((JsonObject)element);
+ queue.add(schema);
+ object.setAdditionalPropertiesSchema(schema);
+ }
+ };
+ }
+
+ private static MyReader createPropertyNames() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonSchemaObject schema = new JsonSchemaObject((JsonObject)element);
+ queue.add(schema);
+ object.setPropertyNamesSchema(schema);
+ }
+ };
+ }
+
+ private static MyReader createRequired() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonArray) {
+ object.setRequired(((JsonArray)element).getValueList().stream()
+ .filter(notEmptyString())
+ .map(el -> StringUtil.unquoteString(el.getText())).collect(Collectors.toSet()));
+ }
+ };
+ }
+
+ private static MyReader createItems() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonSchemaObject schema = new JsonSchemaObject((JsonObject)element);
+ queue.add(schema);
+ object.setItemsSchema(schema);
+ } else if (element instanceof JsonArray) {
+ final List<JsonSchemaObject> list = new ArrayList<>();
+ final List<JsonValue> values = ((JsonArray)element).getValueList();
+ for (JsonValue value : values) {
+ if (value instanceof JsonObject) {
+ final JsonSchemaObject child = new JsonSchemaObject((JsonObject)value);
+ queue.add(child);
+ list.add(child);
+ }
+ }
+ object.setItemsSchemaList(list);
+ }
+ };
+ }
+
+ private static MyReader createContains() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonSchemaObject schema = new JsonSchemaObject((JsonObject)element);
+ queue.add(schema);
+ object.setContainsSchema(schema);
+ }
+ };
+ }
+
+ private static MyReader createAdditionalItems() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonBooleanLiteral) {
+ object.setAdditionalItemsAllowed(((JsonBooleanLiteral)element).getValue());
+ } else if (element instanceof JsonObject) {
+ final JsonSchemaObject additionalItemsSchema = new JsonSchemaObject((JsonObject)element);
+ queue.add(additionalItemsSchema);
+ object.setAdditionalItemsSchema(additionalItemsSchema);
+ }
+ };
+ }
+
+ private static MyReader createPropertiesConsumer() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ object.setProperties(readInnerObject((JsonObject)element, queue));
+ }
+ };
+ }
+
+ private static MyReader createDefinitionsConsumer() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonObject definitions = (JsonObject)element;
+ object.setDefinitionsMap(readInnerObject(definitions, queue));
+ }
+ };
+ }
+
+ @NotNull
+ private static Map<String, JsonSchemaObject> readInnerObject(@NotNull JsonObject element,
+ @NotNull Collection<JsonSchemaObject> queue) {
+ final List<JsonProperty> properties = element.getPropertyList();
+ final Map<String, JsonSchemaObject> map = new HashMap<>();
+ for (JsonProperty property : properties) {
+ if (!(property.getValue() instanceof JsonObject)) continue;
+ final JsonSchemaObject child = new JsonSchemaObject((JsonObject)property.getValue());
+ queue.add(child);
+ map.put(property.getName(), child);
+ }
+ return map;
+ }
+
+ private static MyReader createDefault() {
+ return (element, object, queue) -> {
+ if (element instanceof JsonObject) {
+ final JsonSchemaObject schemaObject = new JsonSchemaObject((JsonObject)element);
+ queue.add(schemaObject);
+ object.setDefault(schemaObject);
+ } else if (element instanceof JsonStringLiteral) {
+ object.setDefault(StringUtil.unquoteString(((JsonStringLiteral)element).getValue()));
+ } else if (element instanceof JsonNumberLiteral) {
+ object.setDefault(getNumber((JsonNumberLiteral) element));
+ } else if (element instanceof JsonBooleanLiteral) {
+ object.setDefault(((JsonBooleanLiteral)element).getValue());
+ }
+ };
+ }
+
+ private interface MyReader {
+ void read(@NotNull JsonElement source, @NotNull JsonSchemaObject target, @NotNull Collection<JsonSchemaObject> processingQueue);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReferenceContributor.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReferenceContributor.java
new file mode 100644
index 00000000..9ff7243c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReferenceContributor.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2000-2017 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.codeInsight.completion.CompletionUtil;
+import com.intellij.json.psi.*;
+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.intellij.util.ObjectUtils;
+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 JsonSchemaReferenceContributor extends PsiReferenceContributor {
+ public static final PsiElementPattern.Capture<JsonValue> REF_PATTERN = createPropertyValuePattern("$ref", true, false);
+ public static final PsiElementPattern.Capture<JsonValue> SCHEMA_PATTERN = createPropertyValuePattern("$schema", false, true);
+ public static final PsiElementPattern.Capture<JsonStringLiteral> REQUIRED_PROP_PATTERN = createRequiredPropPattern();
+
+ @Override
+ public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
+ registrar.registerReferenceProvider(REF_PATTERN, new JsonPointerReferenceProvider(false));
+ registrar.registerReferenceProvider(SCHEMA_PATTERN, new JsonPointerReferenceProvider(true));
+ registrar.registerReferenceProvider(REQUIRED_PROP_PATTERN, new JsonRequiredPropsReferenceProvider());
+ }
+
+ private static PsiElementPattern.Capture<JsonValue> createPropertyValuePattern(
+ @SuppressWarnings("SameParameterValue") @NotNull final String propertyName, boolean schemaOnly, boolean rootOnly) {
+
+ return PlatformPatterns.psiElement(JsonValue.class).and(new FilterPattern(new ElementFilter() {
+ @Override
+ public boolean isAcceptable(Object element, @Nullable PsiElement context) {
+ if (element instanceof JsonValue) {
+ final JsonValue value = (JsonValue) element;
+ if (schemaOnly && !JsonSchemaService.isSchemaFile(CompletionUtil.getOriginalOrSelf(value.getContainingFile()))) return false;
+
+ final JsonProperty property = ObjectUtils.tryCast(value.getParent(), JsonProperty.class);
+ if (property != null && property.getValue() == element) {
+ final PsiFile file = property.getContainingFile();
+ if (rootOnly && (!(file instanceof JsonFile) || ((JsonFile)file).getTopLevelValue() != property.getParent())) return false;
+ return propertyName.equals(property.getName());
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isClassAcceptable(Class hintClass) {
+ return true;
+ }
+ }));
+ }
+
+ private static PsiElementPattern.Capture<JsonStringLiteral> createRequiredPropPattern() {
+ return PlatformPatterns.psiElement(JsonStringLiteral.class).and(new FilterPattern(new ElementFilter() {
+ @Override
+ public boolean isAcceptable(Object element, @Nullable PsiElement context) {
+ if (!(element instanceof JsonStringLiteral)) return false;
+ if (!JsonSchemaService.isSchemaFile(((JsonStringLiteral)element).getContainingFile())) return false;
+ final PsiElement parent = ((JsonStringLiteral)element).getParent();
+ if (!(parent instanceof JsonArray)) return false;
+ PsiElement property = parent.getParent();
+ if (!(property instanceof JsonProperty)) return false;
+ return "required".equals(((JsonProperty)property).getName());
+ }
+
+ @Override
+ public boolean isClassAcceptable(Class hintClass) {
+ return true;
+ }
+ }));
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaRegexInjector.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaRegexInjector.java
new file mode 100644
index 00000000..588551ca
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaRegexInjector.java
@@ -0,0 +1,68 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.json.psi.JsonStringLiteral;
+import com.intellij.lang.injection.MultiHostInjector;
+import com.intellij.lang.injection.MultiHostRegistrar;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiLanguageInjectionHost;
+import com.intellij.util.ThreeState;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.intellij.lang.regexp.ecmascript.EcmaScriptRegexpLanguage;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class JsonSchemaRegexInjector implements MultiHostInjector {
+ @Override
+ public void getLanguagesToInject(@NotNull MultiHostRegistrar registrar, @NotNull PsiElement context) {
+ if (!JsonSchemaService.isSchemaFile(context.getContainingFile())) return;
+ JsonOriginalPsiWalker walker = JsonLikePsiWalker.JSON_ORIGINAL_PSI_WALKER;
+ if (!(context instanceof JsonStringLiteral)) return;
+ ThreeState isName = walker.isName(context);
+ List<JsonSchemaVariantsTreeBuilder.Step> position = walker.findPosition(context, isName == ThreeState.NO);
+ if (position == null || position.isEmpty()) return;
+ if (isName == ThreeState.YES) {
+ JsonSchemaVariantsTreeBuilder.Step lastStep = ContainerUtil.getLastItem(position);
+ if (lastStep != null && "patternProperties".equals(lastStep.getName())) {
+ if (isNestedInPropertiesList(position)) return;
+ injectForHost(registrar, (JsonStringLiteral)context);
+ }
+ }
+ else if (isName == ThreeState.NO) {
+ JsonSchemaVariantsTreeBuilder.Step lastStep = ContainerUtil.getLastItem(position);
+ if (lastStep != null && "pattern".equals(lastStep.getName())) {
+ if (isNestedInPropertiesList(position)) return;
+ injectForHost(registrar, (JsonStringLiteral)context);
+ }
+ }
+ }
+
+ private static boolean isNestedInPropertiesList(List<JsonSchemaVariantsTreeBuilder.Step> position) {
+ if (position.size() >= 2) {
+ JsonSchemaVariantsTreeBuilder.Step prev = position.get(position.size() - 2);
+ if ("properties".equals(prev.getName())) return true;
+ }
+ return false;
+ }
+
+ private static void injectForHost(@NotNull MultiHostRegistrar registrar, @NotNull JsonStringLiteral host) {
+ List<Pair<TextRange, String>> fragments = host.getTextFragments();
+ if (fragments.isEmpty()) return;
+ registrar.startInjecting(EcmaScriptRegexpLanguage.INSTANCE);
+ for (Pair<TextRange, String> fragment : fragments) {
+ registrar.addPlace(null, null, (PsiLanguageInjectionHost)host, fragment.first);
+ }
+ registrar.doneInjecting();
+ }
+
+ @NotNull
+ @Override
+ public List<? extends Class<? extends PsiElement>> elementsToInjectIn() {
+ return ContainerUtil.list(JsonStringLiteral.class);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaResolver.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaResolver.java
new file mode 100644
index 00000000..269b94af
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaResolver.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2000-2017 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.json.psi.*;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.jetbrains.jsonSchema.impl.JsonSchemaAnnotatorChecker.areSchemaTypesCompatible;
+
+/**
+ * @author Irina.Chernushina on 4/24/2017.
+ */
+public class JsonSchemaResolver {
+ @NotNull private final JsonSchemaObject mySchema;
+ private final boolean myIsName;
+ @NotNull private final List<JsonSchemaVariantsTreeBuilder.Step> myPosition;
+
+ public JsonSchemaResolver(@NotNull JsonSchemaObject schema, boolean isName, @NotNull List<JsonSchemaVariantsTreeBuilder.Step> position) {
+ mySchema = schema;
+ myIsName = isName;
+ myPosition = position;
+ }
+
+ public JsonSchemaResolver(@NotNull JsonSchemaObject schema) {
+ mySchema = schema;
+ myIsName = true;
+ myPosition = Collections.emptyList();
+ }
+
+ public MatchResult detailedResolve() {
+ final JsonSchemaTreeNode node = JsonSchemaVariantsTreeBuilder.buildTree(mySchema, myPosition, false, false, !myIsName);
+ return MatchResult.create(node);
+ }
+
+ @NotNull
+ public Collection<JsonSchemaObject> resolve() {
+ final MatchResult result = detailedResolve();
+ final List<JsonSchemaObject> list = new ArrayList<>(result.mySchemas);
+ list.addAll(result.myExcludingSchemas.stream().flatMap(Collection::stream).collect(Collectors.toList()));
+ return list;
+ }
+
+ @Nullable
+ public PsiElement findNavigationTarget(boolean literalResolve,
+ @Nullable final JsonValue element,
+ boolean acceptAdditionalPropertiesSchema) {
+ final JsonSchemaTreeNode node = JsonSchemaVariantsTreeBuilder
+ .buildTree(mySchema, myPosition, true, literalResolve, acceptAdditionalPropertiesSchema || !myIsName);
+ return getSchemaNavigationItem(selectSchema(node, element, myPosition.isEmpty()));
+ }
+
+ @Nullable
+ private static JsonSchemaObject selectSchema(@NotNull final JsonSchemaTreeNode resolveRoot,
+ @Nullable final JsonValue element, boolean topLevelSchema) {
+ final MatchResult matchResult = MatchResult.create(resolveRoot);
+ List<JsonSchemaObject> schemas = new ArrayList<>(matchResult.mySchemas);
+ schemas.addAll(matchResult.myExcludingSchemas.stream().flatMap(Collection::stream).collect(Collectors.toList()));
+
+ final JsonSchemaObject firstSchema = getFirstValidSchema(schemas);
+ if (element == null || schemas.size() == 1 || firstSchema == null) {
+ return firstSchema;
+ }
+ // actually we pass any schema here
+ final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(element, firstSchema);
+ JsonValueAdapter adapter;
+ if (walker == null || (adapter = walker.createValueAdapter(element)) == null) return null;
+
+ final JsonValueAdapter parentAdapter;
+ if (topLevelSchema) {
+ parentAdapter = null;
+ } else {
+ final JsonValue parentValue = PsiTreeUtil.getParentOfType(PsiTreeUtil.getParentOfType(element, JsonProperty.class),
+ JsonObject.class, JsonArray.class);
+ if (parentValue == null || (parentAdapter = walker.createValueAdapter(parentValue)) == null) return null;
+ }
+
+ final Ref<JsonSchemaObject> schemaRef = new Ref<>();
+ MatchResult.iterateTree(resolveRoot, node -> {
+ final JsonSchemaTreeNode parent = node.getParent();
+ if (node.getSchema() == null || parentAdapter != null && parent != null && parent.isNothing()) return true;
+ if (!isCorrect(adapter, node.getSchema())) return true;
+ if (parentAdapter == null ||
+ parent == null ||
+ parent.getSchema() == null ||
+ parent.isAny() ||
+ isCorrect(parentAdapter, parent.getSchema())) {
+ schemaRef.set(node.getSchema());
+ return false;
+ }
+ return true;
+ });
+ return schemaRef.get();
+ }
+
+ @Nullable
+ private static JsonSchemaObject getFirstValidSchema(List<JsonSchemaObject> schemas) {
+ return schemas.stream().filter(schema -> schema.getJsonObject().isValid()).findFirst().orElse(null);
+ }
+
+ private static boolean isCorrect(@NotNull final JsonValueAdapter value, @NotNull final JsonSchemaObject schema) {
+ if (!schema.getJsonObject().isValid()) return false;
+ final JsonSchemaType type = JsonSchemaType.getType(value);
+ if (type == null) return true;
+ if (!areSchemaTypesCompatible(schema, type)) return false;
+ final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(JsonComplianceCheckerOptions.RELAX_ENUM_CHECK);
+ checker.checkByScheme(value, schema);
+ return checker.isCorrect();
+ }
+
+ @Nullable
+ private static JsonValue getSchemaNavigationItem(@Nullable final JsonSchemaObject schema) {
+ if (schema == null) return null;
+ final JsonContainer jsonObject = schema.getJsonObject();
+ if (jsonObject.getParent() instanceof JsonProperty) {
+ return ((JsonProperty)jsonObject.getParent()).getNameElement();
+ }
+ return jsonObject;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceImpl.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceImpl.java
new file mode 100644
index 00000000..0c07625c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceImpl.java
@@ -0,0 +1,488 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
+import com.intellij.json.JsonUtil;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.ClearableLazyValue;
+import com.intellij.openapi.util.Factory;
+import com.intellij.openapi.util.ModificationTracker;
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
+import com.intellij.util.containers.ConcurrentList;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.containers.MultiMap;
+import com.intellij.util.messages.MessageBusConnection;
+import com.jetbrains.jsonSchema.JsonSchemaCatalogProjectConfiguration;
+import com.jetbrains.jsonSchema.JsonSchemaVfsListener;
+import com.jetbrains.jsonSchema.extension.*;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.remote.JsonFileResolver;
+import com.jetbrains.jsonSchema.remote.JsonSchemaCatalogManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+public class JsonSchemaServiceImpl implements JsonSchemaService {
+ @NotNull private final Project myProject;
+ @NotNull private final MyState myState;
+ @NotNull private final ClearableLazyValue<Set<String>> myBuiltInSchemaIds;
+ @NotNull private final Set<String> myRefs = ContainerUtil.newConcurrentSet();
+ private final AtomicLong myAnyChangeCount = new AtomicLong(0);
+ private final ModificationTracker myAnySchemaChangeTracker;
+
+ @NotNull private final JsonSchemaCatalogManager myCatalogManager;
+
+ public JsonSchemaServiceImpl(@NotNull Project project) {
+ myProject = project;
+ myState = new MyState(() -> getProvidersFromFactories(), myProject);
+ myBuiltInSchemaIds = new ClearableLazyValue<Set<String>>() {
+ @NotNull
+ @Override
+ protected Set<String> compute() {
+ return myState.getFiles().stream().map(f -> JsonCachedValues.getSchemaId(f, myProject)).collect(Collectors.toSet());
+ }
+ };
+ myAnySchemaChangeTracker = () -> myAnyChangeCount.get();
+ myCatalogManager = new JsonSchemaCatalogManager(myProject);
+
+ MessageBusConnection connection = project.getMessageBus().connect();
+ connection.subscribe(JsonSchemaVfsListener.JSON_SCHEMA_CHANGED, myAnyChangeCount::incrementAndGet);
+ connection.subscribe(JsonSchemaVfsListener.JSON_DEPS_CHANGED, () -> {
+ myRefs.clear();
+ myAnyChangeCount.incrementAndGet();
+ });
+ JsonSchemaVfsListener.startListening(project, this, connection);
+ myCatalogManager.startUpdates();
+ }
+
+ @Override
+ public ModificationTracker getAnySchemaChangeTracker() {
+ return myAnySchemaChangeTracker;
+ }
+
+ private List<JsonSchemaFileProvider> getProvidersFromFactories() {
+ List<JsonSchemaFileProvider> providers = new ArrayList<>();
+ for (JsonSchemaProviderFactory factory : getProviderFactories()) {
+ try {
+ providers.addAll(factory.getProviders(myProject));
+ }
+ catch (Exception e) {
+ Logger.getInstance(JsonSchemaService.class).error(e);
+ }
+ }
+ return providers;
+ }
+
+ @NotNull
+ protected JsonSchemaProviderFactory[] getProviderFactories() {
+ return JsonSchemaProviderFactory.EP_NAME.getExtensions();
+ }
+
+ @Nullable
+ @Override
+ public JsonSchemaFileProvider getSchemaProvider(@NotNull VirtualFile schemaFile) {
+ return myState.getProvider(schemaFile);
+ }
+
+ @Override
+ public void reset() {
+ myState.reset();
+ myBuiltInSchemaIds.drop();
+ myAnyChangeCount.incrementAndGet();
+ for (Runnable action: myResetActions) {
+ action.run();
+ }
+ DaemonCodeAnalyzer.getInstance(myProject).restart();
+ }
+
+ @Override
+ @Nullable
+ public VirtualFile findSchemaFileByReference(@NotNull String reference, @Nullable VirtualFile referent) {
+ final Optional<VirtualFile> optional = findBuiltInSchemaByReference(reference);
+ return optional.orElseGet(() -> {
+ if (reference.startsWith("#")) return referent;
+ return JsonFileResolver.resolveSchemaByReference(referent, JsonSchemaService.normalizeId(reference));
+ });
+ }
+
+ private Optional<VirtualFile> findBuiltInSchemaByReference(@NotNull String reference) {
+ String id = JsonSchemaService.normalizeId(reference);
+ if (!myBuiltInSchemaIds.getValue().contains(id)) return Optional.empty();
+ return myState.getFiles().stream()
+ .filter(file -> id.equals(JsonCachedValues.getSchemaId(file, myProject)))
+ .findFirst();
+ }
+
+ @Override
+ @NotNull
+ public Collection<VirtualFile> getSchemaFilesForFile(@NotNull final VirtualFile file) {
+ return getSchemasForFile(file, false, false);
+ }
+
+ @NotNull
+ public Collection<VirtualFile> getSchemasForFile(@NotNull VirtualFile file, boolean single, boolean onlyUserSchemas) {
+ String schemaUrl = null;
+ if (!onlyUserSchemas) {
+ // prefer schema-schema if it is specified in "$schema" property
+ schemaUrl = JsonCachedValues.getSchemaUrlFromSchemaProperty(file, myProject);
+ if (isSchemaUrl(schemaUrl)) {
+ final VirtualFile virtualFile = resolveFromSchemaProperty(schemaUrl, file);
+ if (virtualFile != null) return Collections.singletonList(virtualFile);
+ }
+ }
+
+
+ List<JsonSchemaFileProvider> providers = getProvidersForFile(file);
+
+ // proper priority:
+ // 1) user providers
+ // 2) $schema property
+ // 3) built-in providers
+ // 4) schema catalog
+
+ boolean checkSchemaProperty = true;
+ if (!onlyUserSchemas && providers.stream().noneMatch(p -> p.getSchemaType() == SchemaType.userSchema)) {
+ if (schemaUrl == null) schemaUrl = JsonCachedValues.getSchemaUrlFromSchemaProperty(file, myProject);
+ VirtualFile virtualFile = resolveFromSchemaProperty(schemaUrl, file);
+ if (virtualFile != null) return Collections.singletonList(virtualFile);
+ checkSchemaProperty = false;
+ }
+
+ if (!single) {
+ List<VirtualFile> files = ContainerUtil.newArrayList();
+ for (JsonSchemaFileProvider provider : providers) {
+ VirtualFile schemaFile = getSchemaForProvider(myProject, provider);
+ if (schemaFile != null) {
+ files.add(schemaFile);
+ }
+ }
+ if (!files.isEmpty()) {
+ return files;
+ }
+ }
+ else if (!providers.isEmpty()) {
+ final JsonSchemaFileProvider selected;
+ if (providers.size() > 2) return ContainerUtil.emptyList();
+ if (providers.size() > 1) {
+ final Optional<JsonSchemaFileProvider> userSchema =
+ providers.stream().filter(provider -> SchemaType.userSchema.equals(provider.getSchemaType())).findFirst();
+ if (!userSchema.isPresent()) return ContainerUtil.emptyList();
+ selected = userSchema.get();
+ } else selected = providers.get(0);
+ VirtualFile schemaFile = getSchemaForProvider(myProject, selected);
+ return ContainerUtil.createMaybeSingletonList(schemaFile);
+ }
+
+ if (onlyUserSchemas) {
+ return ContainerUtil.emptyList();
+ }
+
+ if (checkSchemaProperty) {
+ if (schemaUrl == null) schemaUrl = JsonCachedValues.getSchemaUrlFromSchemaProperty(file, myProject);
+ VirtualFile virtualFile = resolveFromSchemaProperty(schemaUrl, file);
+ if (virtualFile != null) return Collections.singletonList(virtualFile);
+ }
+
+ return ContainerUtil.createMaybeSingletonList(resolveSchemaFromOtherSources(file));
+ }
+
+ @NotNull
+ public List<JsonSchemaFileProvider> getProvidersForFile(@NotNull VirtualFile file) {
+ return myState.getProviders().stream().filter(provider -> isProviderAvailable(file, provider)).collect(
+ Collectors.toList());
+ }
+
+ @Nullable
+ private VirtualFile resolveFromSchemaProperty(@Nullable String schemaUrl, @NotNull VirtualFile file) {
+ if (schemaUrl != null) {
+ VirtualFile virtualFile = findSchemaFileByReference(schemaUrl, file);
+ if (virtualFile != null) return virtualFile;
+ }
+ return null;
+ }
+
+ @Override
+ public List<JsonSchemaInfo> getAllUserVisibleSchemas() {
+ List<String> schemas = myCatalogManager.getAllCatalogSchemas();
+ Collection<? extends JsonSchemaFileProvider> providers = myState.getProviders();
+ List<JsonSchemaInfo> results = ContainerUtil.newArrayListWithCapacity(schemas.size() + providers.size());
+ Set<String> processedRemotes = ContainerUtil.newHashSet();
+ for (JsonSchemaFileProvider provider: providers) {
+ if (provider.isUserVisible()) {
+ if (provider.getRemoteSource() != null) {
+ if (processedRemotes.add(provider.getRemoteSource())) {
+ results.add(new JsonSchemaInfo(provider));
+ }
+ }
+ else {
+ results.add(new JsonSchemaInfo(provider));
+ }
+ }
+ }
+
+ for (String schema: schemas) {
+ if (processedRemotes.add(schema)) {
+ results.add(new JsonSchemaInfo(schema));
+ }
+ }
+ return results;
+ }
+
+ @Nullable
+ @Override
+ public JsonSchemaObject getSchemaObject(@NotNull final VirtualFile file) {
+ Collection<VirtualFile> schemas = getSchemasForFile(file, true, false);
+ if (schemas.size() == 0) return null;
+ assert schemas.size() == 1;
+ VirtualFile schemaFile = schemas.iterator().next();
+ return JsonCachedValues.getSchemaObject(replaceHttpFileWithBuiltinIfNeeded(schemaFile), myProject);
+ }
+
+ public VirtualFile replaceHttpFileWithBuiltinIfNeeded(VirtualFile schemaFile) {
+ // this hack is needed to handle user-defined mappings via urls
+ // we cannot perform that inside corresponding provider, because it leads to recursive component dependency
+ // this way we're preventing http files when a built-in schema exists
+ if (!JsonSchemaCatalogProjectConfiguration.getInstance(myProject).isPreferRemoteSchemas()
+ && schemaFile instanceof HttpVirtualFile) {
+ String url = schemaFile.getUrl();
+ VirtualFile first1 = getLocalSchemaByUrl(url);
+ return first1 != null ? first1 : schemaFile;
+ }
+ return schemaFile;
+ }
+
+ @Nullable
+ public VirtualFile getLocalSchemaByUrl(String url) {
+ return myState.getFiles().stream()
+ .filter(f -> {
+ JsonSchemaFileProvider prov = getSchemaProvider(f);
+ return prov != null && !(prov.getSchemaFile() instanceof HttpVirtualFile)
+ && (url.equals(prov.getRemoteSource()) || JsonFileResolver.replaceUnsafeSchemaStoreUrls(url).equals(prov.getRemoteSource())
+ || url.equals(JsonFileResolver.replaceUnsafeSchemaStoreUrls(prov.getRemoteSource())));
+ }).findFirst().orElse(null);
+ }
+
+ @Nullable
+ @Override
+ public JsonSchemaObject getSchemaObjectForSchemaFile(@NotNull VirtualFile schemaFile) {
+ return JsonCachedValues.getSchemaObject(schemaFile, myProject);
+ }
+
+ @Override
+ public boolean isSchemaFile(@NotNull VirtualFile file) {
+ return JsonUtil.isJsonFile(file) && (isMappedSchema(file)
+ || isSchemaByProvider(file)
+ || hasSchemaSchema(file));
+ }
+
+ private boolean isMappedSchema(@NotNull VirtualFile file) {
+ return isMappedSchema(file, true);
+ }
+
+ public boolean isMappedSchema(@NotNull VirtualFile file, boolean canRecompute) {
+ return (canRecompute || myState.isComputed()) && myState.getFiles().contains(file);
+ }
+
+ private boolean isSchemaByProvider(@NotNull VirtualFile file) {
+ JsonSchemaFileProvider provider = myState.getProvider(file);
+ if (provider == null) {
+ for (JsonSchemaFileProvider stateProvider: myState.getProviders()) {
+ if (isSchemaProvider(stateProvider) && stateProvider.isAvailable(file))
+ return true;
+ }
+ return false;
+ }
+ return isSchemaProvider(provider);
+ }
+
+ private static boolean isSchemaProvider(JsonSchemaFileProvider provider) {
+ VirtualFile schemaFile = provider.getSchemaFile();
+ if (!(schemaFile instanceof HttpVirtualFile)) return false;
+ String url = schemaFile.getUrl();
+ return isSchemaUrl(url);
+ }
+
+ private static boolean isSchemaUrl(@Nullable String url) {
+ return url != null && url.startsWith("http://json-schema.org/") && (url.endsWith("/schema") || url.endsWith("/schema#"));
+ }
+
+ @Override
+ public JsonSchemaVersion getSchemaVersion(@NotNull VirtualFile file) {
+ if (isMappedSchema(file)) {
+ JsonSchemaFileProvider provider = myState.getProvider(file);
+ if (provider != null) {
+ return provider.getSchemaVersion();
+ }
+ }
+
+ return getSchemaVersionFromSchemaUrl(file);
+ }
+
+ @Nullable
+ private JsonSchemaVersion getSchemaVersionFromSchemaUrl(@NotNull VirtualFile file) {
+ Ref<String> res = Ref.create(null);
+ //noinspection CodeBlock2Expr
+ ApplicationManager.getApplication().runReadAction(() -> {
+ res.set(JsonCachedValues.getSchemaUrlFromSchemaProperty(file, myProject));
+ });
+ if (res.isNull()) return null;
+ return JsonSchemaVersion.byId(res.get());
+ }
+
+ private boolean hasSchemaSchema(VirtualFile file) {
+ return getSchemaVersionFromSchemaUrl(file) != null;
+ }
+
+ private static boolean isProviderAvailable(@NotNull final VirtualFile file, @NotNull JsonSchemaFileProvider provider) {
+ return provider.isAvailable(file);
+ }
+
+ @Nullable
+ private VirtualFile resolveSchemaFromOtherSources(@NotNull VirtualFile file) {
+ return myCatalogManager.getSchemaFileForFile(file);
+ }
+
+ @Override
+ public void registerRemoteUpdateCallback(Runnable callback) {
+ myCatalogManager.registerCatalogUpdateCallback(callback);
+ }
+
+ @Override
+ public void unregisterRemoteUpdateCallback(Runnable callback) {
+ myCatalogManager.unregisterCatalogUpdateCallback(callback);
+ }
+
+ private final ConcurrentList<Runnable> myResetActions = ContainerUtil.createConcurrentList();
+
+ @Override
+ public void registerResetAction(Runnable action) {
+ myResetActions.add(action);
+ }
+
+ @Override
+ public void unregisterResetAction(Runnable action) {
+ myResetActions.remove(action);
+ }
+
+ @Override
+ public void registerReference(String ref) {
+ int index = StringUtil.lastIndexOfAny(ref, "\\/");
+ if (index >= 0) {
+ ref = ref.substring(index + 1);
+ }
+ myRefs.add(ref);
+ }
+
+ @Override
+ public boolean possiblyHasReference(String ref) {
+ return myRefs.contains(ref);
+ }
+
+ @Override
+ public void triggerUpdateRemote() {
+ myCatalogManager.triggerUpdateCatalog(myProject);
+ }
+
+ @Override
+ public boolean isApplicableToFile(@Nullable VirtualFile file) {
+ if (file == null) return false;
+ for (JsonSchemaEnabler e : JsonSchemaEnabler.EXTENSION_POINT_NAME.getExtensionList()) {
+ if (e.isEnabledForFile(file)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static class MyState {
+ @NotNull private final Factory<List<JsonSchemaFileProvider>> myFactory;
+ @NotNull private final Project myProject;
+ @NotNull private final ClearableLazyValue<MultiMap<VirtualFile, JsonSchemaFileProvider>> myData;
+ private final AtomicBoolean myIsComputed = new AtomicBoolean(false);
+
+ private MyState(@NotNull final Factory<List<JsonSchemaFileProvider>> factory, @NotNull Project project) {
+ myFactory = factory;
+ myProject = project;
+ myData = new ClearableLazyValue<MultiMap<VirtualFile, JsonSchemaFileProvider>>() {
+ @NotNull
+ @Override
+ public MultiMap<VirtualFile, JsonSchemaFileProvider> compute() {
+ myIsComputed.set(true);
+ return createFileProviderMap(myFactory.create(), myProject);
+ }
+
+ @NotNull
+ @Override
+ public final synchronized MultiMap<VirtualFile, JsonSchemaFileProvider> getValue() {
+ return super.getValue();
+ }
+
+ @Override
+ public final synchronized void drop() {
+ myIsComputed.set(false);
+ super.drop();
+ }
+ };
+ }
+
+ public void reset() {
+ myData.drop();
+ }
+
+ @NotNull
+ public Collection<? extends JsonSchemaFileProvider> getProviders() {
+ return myData.getValue().values();
+ }
+
+ @NotNull
+ public Set<VirtualFile> getFiles() {
+ return myData.getValue().keySet();
+ }
+
+ @Nullable
+ public JsonSchemaFileProvider getProvider(@NotNull final VirtualFile file) {
+ final Collection<JsonSchemaFileProvider> providers = myData.getValue().get(file);
+ return providers.stream().filter(p -> p.getSchemaType() == SchemaType.userSchema).findFirst().orElse(providers.stream().findFirst().orElse(null));
+ }
+
+ public boolean isComputed() {
+ return myIsComputed.get();
+ }
+
+ @NotNull
+ private static MultiMap<VirtualFile, JsonSchemaFileProvider> createFileProviderMap(@NotNull final List<JsonSchemaFileProvider> list,
+ @NotNull Project project) {
+ // if there are different providers with the same schema files,
+ // stream API does not allow to collect same keys with Collectors.toMap(): throws duplicate key
+ final MultiMap<VirtualFile, JsonSchemaFileProvider> map = MultiMap.create();
+ for (JsonSchemaFileProvider provider : list) {
+ VirtualFile schemaFile = getSchemaForProvider(project, provider);
+ if (schemaFile != null) {
+ map.putValue(schemaFile, provider);
+ }
+ }
+ return map;
+ }
+ }
+
+ @Nullable
+ private static VirtualFile getSchemaForProvider(@NotNull Project project, @NotNull JsonSchemaFileProvider provider) {
+ if (JsonSchemaCatalogProjectConfiguration.getInstance(project).isPreferRemoteSchemas()) {
+ final String source = provider.getRemoteSource();
+ if (source != null && !source.endsWith("!")) {
+ return VirtualFileManager.getInstance().findFileByUrl(source);
+ }
+ }
+ return provider.getSchemaFile();
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaTreeNode.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaTreeNode.java
new file mode 100644
index 00000000..76774c46
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaTreeNode.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2000-2017 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.util.SmartList;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 4/20/2017.
+ */
+public class JsonSchemaTreeNode {
+ private boolean myAny;
+ private boolean myNothing;
+ private int myExcludingGroupNumber = -1;
+ @NotNull private SchemaResolveState myResolveState = SchemaResolveState.normal;
+
+ @Nullable private final JsonSchemaObject mySchema;
+ @NotNull private final List<JsonSchemaVariantsTreeBuilder.Step> mySteps = new SmartList<>();
+
+ @Nullable private final JsonSchemaTreeNode myParent;
+ @NotNull private final List<JsonSchemaTreeNode> myChildren = new ArrayList<>();
+
+ public JsonSchemaTreeNode(@Nullable JsonSchemaTreeNode parent,
+ @Nullable JsonSchemaObject schema) {
+ assert schema != null || parent != null;
+ myParent = parent;
+ mySchema = schema;
+ if (parent != null && !parent.getSteps().isEmpty()) {
+ mySteps.addAll(parent.getSteps().subList(1, parent.getSteps().size()));
+ }
+ }
+
+ public void anyChild() {
+ final JsonSchemaTreeNode node = new JsonSchemaTreeNode(this, null);
+ node.myAny = true;
+ myChildren.add(node);
+ }
+
+ public void nothingChild() {
+ final JsonSchemaTreeNode node = new JsonSchemaTreeNode(this, null);
+ node.myNothing = true;
+ myChildren.add(node);
+ }
+
+ public void createChildrenFromOperation(@NotNull JsonSchemaVariantsTreeBuilder.Operation operation) {
+ if (!SchemaResolveState.normal.equals(operation.myState)) {
+ final JsonSchemaTreeNode node = new JsonSchemaTreeNode(this, null);
+ node.myResolveState = operation.myState;
+ myChildren.add(node);
+ return;
+ }
+ if (!operation.myAnyOfGroup.isEmpty()) {
+ myChildren.addAll(convertToNodes(operation.myAnyOfGroup));
+ }
+ if (!operation.myOneOfGroup.isEmpty()) {
+ for (int i = 0; i < operation.myOneOfGroup.size(); i++) {
+ final List<JsonSchemaObject> group = operation.myOneOfGroup.get(i);
+ final List<JsonSchemaTreeNode> children = convertToNodes(group);
+ final int number = i;
+ children.forEach(c -> c.myExcludingGroupNumber = number);
+ myChildren.addAll(children);
+ }
+ }
+ }
+
+ private List<JsonSchemaTreeNode> convertToNodes(List<JsonSchemaObject> children) {
+ List<JsonSchemaTreeNode> nodes = ContainerUtil.newArrayListWithCapacity(children.size());
+ for (JsonSchemaObject child: children) {
+ nodes.add(new JsonSchemaTreeNode(this, child));
+ }
+ return nodes;
+ }
+
+ @NotNull
+ public SchemaResolveState getResolveState() {
+ return myResolveState;
+ }
+
+ public boolean isAny() {
+ return myAny;
+ }
+
+ public boolean isNothing() {
+ return myNothing;
+ }
+
+
+ public void setChild(@NotNull final JsonSchemaObject schema) {
+ myChildren.add(new JsonSchemaTreeNode(this, schema));
+ }
+
+ @Nullable
+ public JsonSchemaObject getSchema() {
+ return mySchema;
+ }
+
+ @NotNull
+ public List<JsonSchemaVariantsTreeBuilder.Step> getSteps() {
+ return mySteps;
+ }
+
+ @Nullable
+ public JsonSchemaTreeNode getParent() {
+ return myParent;
+ }
+
+ @NotNull
+ public List<JsonSchemaTreeNode> getChildren() {
+ return myChildren;
+ }
+
+ public int getExcludingGroupNumber() {
+ return myExcludingGroupNumber;
+ }
+
+ public void setSteps(@NotNull List<JsonSchemaVariantsTreeBuilder.Step> steps) {
+ mySteps.clear();
+ mySteps.addAll(steps);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ JsonSchemaTreeNode node = (JsonSchemaTreeNode)o;
+
+ if (myAny != node.myAny) return false;
+ if (myNothing != node.myNothing) return false;
+ if (myResolveState != node.myResolveState) return false;
+ if (mySchema != null ? !mySchema.equals(node.mySchema) : node.mySchema != null) return false;
+ //noinspection RedundantIfStatement
+ if (!mySteps.equals(node.mySteps)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (myAny ? 1 : 0);
+ result = 31 * result + (myNothing ? 1 : 0);
+ result = 31 * result + myResolveState.hashCode();
+ result = 31 * result + (mySchema != null ? mySchema.hashCode() : 0);
+ result = 31 * result + mySteps.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("NODE#" + hashCode() + "\n");
+ sb.append(mySteps.stream().map(Object::toString).collect(Collectors.joining("->", "steps: <", ">")));
+ sb.append("\n");
+ if (myExcludingGroupNumber >= 0) sb.append("in excluding group\n");
+ if (myAny) sb.append("any");
+ else if (myNothing) sb.append("nothing");
+ else if (!SchemaResolveState.normal.equals(myResolveState)) sb.append(myResolveState.name());
+ else {
+ assert mySchema != null;
+ final String name = mySchema.getSchemaFile().getName();
+ sb.append("schema from file: ").append(name).append("\n");
+ if (mySchema.getRef() != null) sb.append("$ref: ").append(mySchema.getRef()).append("\n");
+ else if (!mySchema.getProperties().isEmpty()) {
+ sb.append("properties: ");
+ sb.append(String.join(", ", mySchema.getProperties().keySet())).append("\n");
+ }
+ if (!myChildren.isEmpty()) {
+ sb.append("OR children of NODE#").append(hashCode()).append(":\n----------------\n")
+ .append(myChildren.stream().map(Object::toString).collect(Collectors.joining("\n")))
+ .append("\n=================\n");
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaType.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaType.java
new file mode 100644
index 00000000..98351d18
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaType.java
@@ -0,0 +1,84 @@
+package com.jetbrains.jsonSchema.impl;
+
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Irina.Chernushina on 7/15/2015.
+ */
+public enum JsonSchemaType {
+ _string, _number, _integer, _object, _array, _boolean, _null, _any, _string_number;
+
+ public String getName() {
+ return name().substring(1);
+ }
+
+ public String getDefaultValue() {
+ switch (this) {
+ case _string:
+ return "\"\"";
+ case _number:
+ case _integer:
+ case _string_number:
+ return "0";
+ case _object:
+ return "{}";
+ case _array:
+ return "[]";
+ case _boolean:
+ return "false";
+ case _null:
+ return "null";
+ case _any:
+ default:
+ return "";
+ }
+ }
+
+ public boolean isSimple() {
+ switch (this) {
+ case _string:
+ case _number:
+ case _integer:
+ case _boolean:
+ case _null:
+ return true;
+ case _object:
+ case _array:
+ case _any:
+ default:
+ return false;
+ }
+ }
+
+ @Nullable
+ static JsonSchemaType getType(@NotNull final JsonValueAdapter value) {
+ if (value.isNull()) return _null;
+ if (value.isBooleanLiteral()) return _boolean;
+ if (value.isStringLiteral()) {
+ return value.isNumberLiteral() ? _string_number : _string;
+ }
+ if (value.isArray()) return _array;
+ if (value.isObject()) return _object;
+ if (value.isNumberLiteral()) {
+ return isInteger(value.getDelegate().getText()) ? _integer : _number;
+ }
+ return null;
+ }
+
+ public static boolean isInteger(@NotNull String text) {
+ try {
+ Integer.parseInt(text);
+ return true;
+ }
+ catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ public String getDescription() {
+ if (this == _any) return "*";
+ return getName();
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaUsageTriggerCollector.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaUsageTriggerCollector.java
new file mode 100644
index 00000000..98ed82cd
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaUsageTriggerCollector.java
@@ -0,0 +1,13 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.internal.statistic.service.fus.collectors.ApplicationUsageTriggerCollector;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonSchemaUsageTriggerCollector extends ApplicationUsageTriggerCollector {
+ @NotNull
+ @Override
+ public String getGroupId() {
+ return "statistics.json.schema";
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVariantsTreeBuilder.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVariantsTreeBuilder.java
new file mode 100644
index 00000000..1e717be8
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVariantsTreeBuilder.java
@@ -0,0 +1,595 @@
+/*
+ * Copyright 2000-2017 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.json.psi.JsonContainer;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.util.CachedValueProvider;
+import com.intellij.psi.util.CachedValuesManager;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.SmartList;
+import com.intellij.util.ThreeState;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static com.jetbrains.jsonSchema.JsonPointerUtil.*;
+
+/**
+ * @author Irina.Chernushina on 4/20/2017.
+ */
+public class JsonSchemaVariantsTreeBuilder {
+
+ public static JsonSchemaTreeNode buildTree(@NotNull final JsonSchemaObject schema,
+ @Nullable final List<Step> position,
+ final boolean skipLastExpand,
+ final boolean literalResolve,
+ final boolean acceptAdditional) {
+ final JsonSchemaTreeNode root = new JsonSchemaTreeNode(null, schema);
+ JsonSchemaService service = JsonSchemaService.Impl.get(schema.getJsonObject().getProject());
+ expandChildSchema(root, schema, service);
+ // set root's position since this children are just variants of root
+ for (JsonSchemaTreeNode treeNode : root.getChildren()) {
+ treeNode.setSteps(ContainerUtil.notNullize(position));
+ }
+
+ final ArrayDeque<JsonSchemaTreeNode> queue = new ArrayDeque<>(root.getChildren());
+
+ while (!queue.isEmpty()) {
+ final JsonSchemaTreeNode node = queue.removeFirst();
+ if (node.isAny() || node.isNothing() || node.getSteps().isEmpty() || node.getSchema() == null) continue;
+ final Step step = node.getSteps().get(0);
+ if (!typeMatches(step.isFromObject(), node.getSchema())) {
+ node.nothingChild();
+ continue;
+ }
+ if (literalResolve) step.myLiteralResolve = true;
+ final Pair<ThreeState, JsonSchemaObject> pair = step.step(node.getSchema(), acceptAdditional);
+ if (ThreeState.NO.equals(pair.getFirst())) node.nothingChild();
+ else if (ThreeState.YES.equals(pair.getFirst())) node.anyChild();
+ else {
+ // process step results
+ assert pair.getSecond() != null;
+ if (node.getSteps().size() > 1 || !skipLastExpand) expandChildSchema(node, pair.getSecond(), service);
+ else node.setChild(pair.getSecond());
+ }
+
+ queue.addAll(node.getChildren());
+ }
+
+ return root;
+ }
+
+ private static boolean typeMatches(final boolean isObject, @NotNull final JsonSchemaObject schema) {
+ final JsonSchemaType requiredType = isObject ? JsonSchemaType._object : JsonSchemaType._array;
+ if (schema.getType() != null) {
+ return requiredType.equals(schema.getType());
+ }
+ if (schema.getTypeVariants() != null) {
+ for (JsonSchemaType schemaType : schema.getTypeVariants()) {
+ if (requiredType.equals(schemaType)) return true;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private static void expandChildSchema(@NotNull JsonSchemaTreeNode node, @NotNull JsonSchemaObject childSchema, @NotNull JsonSchemaService service) {
+ final JsonContainer element = childSchema.getJsonObject();
+ if (interestingSchema(childSchema)) {
+ final Operation operation =
+ CachedValuesManager.getManager(element.getProject())
+ .createParameterizedCachedValue((JsonSchemaObject param) -> {
+ final Operation expand = new ProcessDefinitionsOperation(param, service);
+ expand.doMap(new HashSet<>());
+ expand.doReduce();
+ return CachedValueProvider.Result.create(expand, element.getContainingFile(),
+ service.getAnySchemaChangeTracker());
+ }, false).getValue(childSchema);
+ node.createChildrenFromOperation(operation);
+ }
+ else {
+ node.setChild(childSchema);
+ }
+ }
+
+ public static List<Step> buildSteps(@NotNull String nameInSchema) {
+ final List<String> chain = split(normalizeSlashes(JsonSchemaService.normalizeId(nameInSchema)));
+ List<Step> steps = ContainerUtil.newArrayListWithCapacity(chain.size());
+ for (String s: chain) {
+ try {
+ steps.add(Step.createArrayElementStep(Integer.parseInt(s)));
+ }
+ catch (NumberFormatException e) {
+ steps.add(Step.createPropertyStep(unescapeJsonPointerPart(s)));
+ }
+ }
+ return steps;
+ }
+
+ static abstract class Operation {
+ @NotNull final List<JsonSchemaObject> myAnyOfGroup = new SmartList<>();
+ @NotNull final List<List<JsonSchemaObject>> myOneOfGroup = new SmartList<>();
+ @NotNull protected final List<Operation> myChildOperations;
+ @NotNull protected final JsonSchemaObject mySourceNode;
+ protected SchemaResolveState myState = SchemaResolveState.normal;
+
+ protected Operation(@NotNull JsonSchemaObject sourceNode) {
+ mySourceNode = sourceNode;
+ myChildOperations = new ArrayList<>();
+ }
+
+ protected abstract void map(@NotNull Set<JsonContainer> visited);
+ protected abstract void reduce();
+
+ public void doMap(@NotNull final Set<JsonContainer> visited) {
+ map(visited);
+ for (Operation operation : myChildOperations) {
+ operation.doMap(visited);
+ }
+ }
+
+ public void doReduce() {
+ if (!SchemaResolveState.normal.equals(myState)) {
+ myChildOperations.clear();
+ myAnyOfGroup.clear();
+ myOneOfGroup.clear();
+ return;
+ }
+
+ // lets do that to make the returned object smaller
+ myAnyOfGroup.forEach(Operation::clearVariants);
+ myOneOfGroup.forEach(list -> list.forEach(Operation::clearVariants));
+
+ for (Operation myChildOperation : myChildOperations) {
+ myChildOperation.doReduce();
+ }
+ reduce();
+ myChildOperations.clear();
+ }
+
+ private static void clearVariants(@NotNull JsonSchemaObject object) {
+ object.setAllOf(null);
+ object.setAnyOf(null);
+ object.setOneOf(null);
+ }
+
+ @Nullable
+ protected Operation createExpandOperation(@NotNull final JsonSchemaObject schema,
+ @NotNull JsonSchemaService service) {
+ if (conflictingSchema(schema)) {
+ final Operation operation = new AnyOfOperation(schema, service);
+ operation.myState = SchemaResolveState.conflict;
+ return operation;
+ }
+ if (schema.getAnyOf() != null) return new AnyOfOperation(schema, service);
+ if (schema.getOneOf() != null) return new OneOfOperation(schema, service);
+ if (schema.getAllOf() != null) return new AllOfOperation(schema, service);
+ return null;
+ }
+
+ protected static List<JsonSchemaObject> mergeOneOf(Operation op) {
+ return op.myOneOfGroup.stream().flatMap(List::stream).collect(Collectors.toList());
+ }
+ }
+
+ // even if there are no definitions to expand, this object may work as an intermediate node in a tree,
+ // connecting oneOf and allOf expansion, for example
+ private static class ProcessDefinitionsOperation extends Operation {
+ private final JsonSchemaService myService;
+
+ protected ProcessDefinitionsOperation(@NotNull JsonSchemaObject sourceNode, JsonSchemaService service) {
+ super(sourceNode);
+ myService = service;
+ }
+
+ @Override
+ public void map(@NotNull final Set<JsonContainer> visited) {
+ JsonSchemaObject current = mySourceNode;
+ while (!StringUtil.isEmptyOrSpaces(current.getRef())) {
+ final JsonSchemaObject definition = current.resolveRefSchema(myService);
+ if (definition == null) {
+ myState = SchemaResolveState.brokenDefinition;
+ return;
+ }
+ // this definition was already expanded; do not cycle
+ if (!visited.add(definition.getJsonObject())) break;
+ current = merge(current, definition, current);
+ }
+ final Operation expandOperation = createExpandOperation(current, myService);
+ if (expandOperation != null) myChildOperations.add(expandOperation);
+ else myAnyOfGroup.add(current);
+ }
+
+ @Override
+ public void reduce() {
+ if (!myChildOperations.isEmpty()) {
+ assert myChildOperations.size() == 1;
+ final Operation operation = myChildOperations.get(0);
+ myAnyOfGroup.addAll(operation.myAnyOfGroup);
+ myOneOfGroup.addAll(operation.myOneOfGroup);
+ }
+ }
+ }
+
+ private static class AllOfOperation extends Operation {
+ private final JsonSchemaService myService;
+
+ protected AllOfOperation(@NotNull JsonSchemaObject sourceNode, JsonSchemaService service) {
+ super(sourceNode);
+ myService = service;
+ }
+
+ @Override
+ public void map(@NotNull final Set<JsonContainer> visited) {
+ List<JsonSchemaObject> allOf = mySourceNode.getAllOf();
+ assert allOf != null;
+ myChildOperations.addAll(ContainerUtil.map(allOf, sourceNode -> new ProcessDefinitionsOperation(sourceNode, myService)));
+ }
+
+ private static <T> int maxSize(List<List<T>> items) {
+ if (items.size() == 0) return 0;
+ int maxsize = -1;
+ for (List<T> item: items) {
+ int size = item.size();
+ if (maxsize < size) maxsize = size;
+ }
+ return maxsize;
+ }
+
+ @Override
+ public void reduce() {
+ myAnyOfGroup.add(mySourceNode);
+
+ for (Operation op : myChildOperations) {
+ if (!op.myState.equals(SchemaResolveState.normal)) continue;
+
+ final List<JsonSchemaObject> mergedAny = andGroups(op.myAnyOfGroup, myAnyOfGroup);
+
+ final List<List<JsonSchemaObject>> mergedExclusive =
+ ContainerUtil.newArrayListWithCapacity(
+ op.myAnyOfGroup.size() * maxSize(myOneOfGroup) +
+ myAnyOfGroup.size() * maxSize(op.myOneOfGroup) +
+ maxSize(myOneOfGroup) * maxSize(op.myOneOfGroup)
+ );
+
+ for (List<JsonSchemaObject> objects : myOneOfGroup) {
+ mergedExclusive.add(andGroups(op.myAnyOfGroup, objects));
+ }
+ for (List<JsonSchemaObject> objects : op.myOneOfGroup) {
+ mergedExclusive.add(andGroups(objects, myAnyOfGroup));
+ }
+ for (List<JsonSchemaObject> group : op.myOneOfGroup) {
+ for (List<JsonSchemaObject> otherGroup : myOneOfGroup) {
+ mergedExclusive.add(andGroups(group, otherGroup));
+ }
+ }
+
+ myAnyOfGroup.clear();
+ myOneOfGroup.clear();
+ myAnyOfGroup.addAll(mergedAny);
+ myOneOfGroup.addAll(mergedExclusive);
+ }
+ }
+ }
+
+ private static List<JsonSchemaObject> andGroups(@NotNull List<JsonSchemaObject> g1,
+ @NotNull List<JsonSchemaObject> g2) {
+ List<JsonSchemaObject> result = ContainerUtil.newArrayListWithCapacity(g1.size() * g2.size());
+ for (JsonSchemaObject s: g1) {
+ result.addAll(andGroup(s, g2));
+ }
+ return result;
+ }
+
+ // here is important, which pointer gets the result: lets make them all different, otherwise two schemas of branches of oneOf would be equal
+ private static List<JsonSchemaObject> andGroup(@NotNull JsonSchemaObject object, @NotNull List<JsonSchemaObject> group) {
+ List<JsonSchemaObject> list = ContainerUtil.newArrayListWithCapacity(group.size());
+ for (JsonSchemaObject s: group) {
+ JsonSchemaObject schemaObject = merge(object, s, s);
+ if (schemaObject.isValidByExclusion()) {
+ list.add(schemaObject);
+ }
+ }
+ return list;
+ }
+
+ private static class OneOfOperation extends Operation {
+ private final JsonSchemaService myService;
+
+ protected OneOfOperation(@NotNull JsonSchemaObject sourceNode, JsonSchemaService service) {
+ super(sourceNode);
+ myService = service;
+ }
+
+ @Override
+ public void map(@NotNull final Set<JsonContainer> visited) {
+ List<JsonSchemaObject> oneOf = mySourceNode.getOneOf();
+ assert oneOf != null;
+ myChildOperations.addAll(ContainerUtil.map(oneOf, sourceNode -> new ProcessDefinitionsOperation(sourceNode, myService)));
+ }
+
+ @Override
+ public void reduce() {
+ final List<JsonSchemaObject> oneOf = new SmartList<>();
+ for (Operation op : myChildOperations) {
+ if (!op.myState.equals(SchemaResolveState.normal)) continue;
+ oneOf.addAll(andGroup(mySourceNode, op.myAnyOfGroup));
+ oneOf.addAll(andGroup(mySourceNode, mergeOneOf(op)));
+ }
+ // here it is not a mistake - all children of this node come to oneOf group
+ myOneOfGroup.add(oneOf);
+ }
+ }
+
+ private static class AnyOfOperation extends Operation {
+ private final JsonSchemaService myService;
+
+ protected AnyOfOperation(@NotNull JsonSchemaObject sourceNode, JsonSchemaService service) {
+ super(sourceNode);
+ myService = service;
+ }
+
+ @Override
+ public void map(@NotNull final Set<JsonContainer> visited) {
+ List<JsonSchemaObject> anyOf = mySourceNode.getAnyOf();
+ assert anyOf != null;
+ myChildOperations.addAll(ContainerUtil.map(anyOf, sourceNode -> new ProcessDefinitionsOperation(sourceNode, myService)));
+ }
+
+ @Override
+ public void reduce() {
+ for (Operation op : myChildOperations) {
+ if (!op.myState.equals(SchemaResolveState.normal)) continue;
+
+ myAnyOfGroup.addAll(andGroup(mySourceNode, op.myAnyOfGroup));
+ for (List<JsonSchemaObject> group : op.myOneOfGroup) {
+ myOneOfGroup.add(andGroup(mySourceNode, group));
+ }
+ }
+ }
+ }
+
+ @NotNull
+ public static JsonSchemaObject merge(@NotNull JsonSchemaObject base,
+ @NotNull JsonSchemaObject other,
+ @NotNull JsonSchemaObject pointTo) {
+ final JsonSchemaObject object = new JsonSchemaObject(pointTo.getJsonObject());
+ object.mergeValues(other);
+ object.mergeValues(base);
+ object.setRef(other.getRef());
+ return object;
+ }
+
+ private static boolean conflictingSchema(JsonSchemaObject schema) {
+ int cnt = 0;
+ if (schema.getAllOf() != null) ++cnt;
+ if (schema.getAnyOf() != null) ++cnt;
+ if (schema.getOneOf() != null) ++cnt;
+ return cnt > 1;
+ }
+
+ private static boolean interestingSchema(@NotNull JsonSchemaObject schema) {
+ return schema.getAnyOf() != null || schema.getOneOf() != null || schema.getAllOf() != null || schema.getRef() != null
+ || schema.getIf() != null;
+ }
+
+ public static class Step {
+ @Nullable private final String myName;
+ private final int myIdx;
+ private boolean myLiteralResolve;
+
+ private Step(@Nullable String name, int idx) {
+ myName = name;
+ myIdx = idx;
+ }
+
+ public static Step createPropertyStep(@NotNull final String name) {
+ return new Step(name, -1);
+ }
+
+ public static Step createArrayElementStep(final int idx) {
+ assert idx >= 0;
+ return new Step(null, idx);
+ }
+
+ public boolean isFromObject() {
+ return myName != null;
+ }
+
+ public boolean isFromArray() {
+ return myName == null;
+ }
+
+ @Nullable
+ public String getName() {
+ return myName;
+ }
+
+ public int getIdx() {
+ return myIdx;
+ }
+
+ @NotNull
+ public Pair<ThreeState, JsonSchemaObject> step(@NotNull JsonSchemaObject parent, boolean acceptAdditionalPropertiesSchemas) {
+ if (myName != null) {
+ return propertyStep(parent, acceptAdditionalPropertiesSchemas);
+ } else {
+ assert myIdx >= 0;
+ return arrayOrNumericPropertyElementStep(parent, acceptAdditionalPropertiesSchemas);
+ }
+ }
+
+ @Override
+ public String toString() {
+ String format = "?%s";
+ if (myName != null) format = "{%s}";
+ if (myIdx >= 0) format = "[%s]";
+ return String.format(format, myName != null ? myName : (myIdx >= 0 ? String.valueOf(myIdx) : "null"));
+ }
+
+ @NotNull
+ private Pair<ThreeState, JsonSchemaObject> propertyStep(@NotNull JsonSchemaObject parent,
+ boolean acceptAdditionalPropertiesSchemas) {
+ assert myName != null;
+ if (JsonSchemaObject.DEFINITIONS.equals(myName) &&
+ parent.getDefinitionsMap() != null && (!isInMainSchema(parent) || myLiteralResolve)) {
+ // definitions pointer here is fictive so lets find any
+ final Map<String, JsonSchemaObject> definitionsMap = parent.getDefinitionsMap();
+ final JsonObject anyDefinitions = definitionsMap.values().stream()
+ .filter(def -> {
+ final JsonProperty parentObj = ObjectUtils.tryCast(def.getJsonObject().getParent(), JsonProperty.class);
+ return parentObj != null && parentObj.isValid() && parentObj.getValue() instanceof JsonObject;
+ })
+ .map(def -> (JsonObject)((JsonProperty) def.getJsonObject().getParent()).getValue())
+ .findFirst().orElse(null);
+ if (anyDefinitions == null) return Pair.create(ThreeState.NO, null);
+ final JsonSchemaObject object = new JsonSchemaObject(anyDefinitions);
+ object.setProperties(definitionsMap);
+ return Pair.create(ThreeState.UNSURE, object);
+ }
+ final JsonSchemaObject child = parent.getProperties().get(myName);
+ if (child != null) {
+ return Pair.create(ThreeState.UNSURE, child);
+ }
+ final JsonSchemaObject schema = parent.getMatchingPatternPropertySchema(myName);
+ if (schema != null) {
+ return Pair.create(ThreeState.UNSURE, schema);
+ }
+ if (acceptAdditionalPropertiesSchemas) {
+ if (parent.getAdditionalPropertiesSchema() != null) {
+ return Pair.create(ThreeState.UNSURE, parent.getAdditionalPropertiesSchema());
+ }
+
+ // resolve inside V7 if-then-else conditionals
+ if (parent.getIf() != null) {
+ JsonSchemaObject childObject;
+
+ // NOTE: do not resolve inside 'if' itself - it is just a condition, but not an actual validation!
+ // only 'then' and 'else' branches provide actual validation sources, but not the 'if' branch
+
+ if (parent.getThen() != null) {
+ childObject = parent.getThen().getProperties().get(myName);
+ if (childObject != null) {
+ return Pair.create(ThreeState.UNSURE, childObject);
+ }
+ }
+ if (parent.getElse() != null) {
+ childObject = parent.getElse().getProperties().get(myName);
+ if (childObject != null) {
+ return Pair.create(ThreeState.UNSURE, childObject);
+ }
+ }
+ }
+ }
+ if (Boolean.FALSE.equals(parent.getAdditionalPropertiesAllowed())) {
+ return Pair.create(ThreeState.NO, null);
+ }
+ // by default, additional properties are allowed
+ return Pair.create(ThreeState.YES, null);
+ }
+
+ private static boolean isInMainSchema(@NotNull JsonSchemaObject parent) {
+ final VirtualFile schemaFile = parent.getSchemaFile();
+ final JsonSchemaService service = JsonSchemaService.Impl.get(parent.getJsonObject().getProject());
+ if (!service.isApplicableToFile(schemaFile) || !service.isSchemaFile(schemaFile)) return false;
+
+ final JsonSchemaObject rootSchema = service.getSchemaObjectForSchemaFile(schemaFile);
+ if (rootSchema == null) return false;
+
+ return JsonSchemaVersion.isSchemaSchemaId(rootSchema.getId());
+ }
+
+ @NotNull
+ private Pair<ThreeState, JsonSchemaObject> arrayOrNumericPropertyElementStep(@NotNull JsonSchemaObject parent,
+ boolean acceptAdditionalPropertiesSchemas) {
+ if (parent.getItemsSchema() != null) {
+ return Pair.create(ThreeState.UNSURE, parent.getItemsSchema());
+ }
+ if (parent.getItemsSchemaList() != null) {
+ final List<JsonSchemaObject> list = parent.getItemsSchemaList();
+ if (myIdx >= 0 && myIdx < list.size()) {
+ return Pair.create(ThreeState.UNSURE, list.get(myIdx));
+ }
+ }
+ final String keyAsString = String.valueOf(myIdx);
+ if (parent.getProperties().containsKey(keyAsString)) {
+ return Pair.create(ThreeState.UNSURE, parent.getProperties().get(keyAsString));
+ }
+ final JsonSchemaObject matchingPatternPropertySchema = parent.getMatchingPatternPropertySchema(keyAsString);
+ if (matchingPatternPropertySchema != null) {
+ return Pair.create(ThreeState.UNSURE, matchingPatternPropertySchema);
+ }
+ if (parent.getAdditionalItemsSchema() != null && acceptAdditionalPropertiesSchemas) {
+ return Pair.create(ThreeState.UNSURE, parent.getAdditionalItemsSchema());
+ }
+ if (Boolean.FALSE.equals(parent.getAdditionalItemsAllowed())) {
+ return Pair.create(ThreeState.NO, null);
+ }
+ return Pair.create(ThreeState.YES, null);
+ }
+ }
+
+ public static class SchemaUrlSplitter {
+ @Nullable
+ private final String mySchemaId;
+ @NotNull
+ private final String myRelativePath;
+
+ public SchemaUrlSplitter(@NotNull final String ref) {
+ if (isSelfReference(ref)) {
+ mySchemaId = null;
+ myRelativePath = "";
+ return;
+ }
+ if (!ref.startsWith("#/")) {
+ int idx = ref.indexOf("#/");
+ if (idx == -1) {
+ mySchemaId = ref.endsWith("#") ? ref.substring(0, ref.length() - 1) : ref;
+ myRelativePath = "";
+ } else {
+ mySchemaId = ref.substring(0, idx);
+ myRelativePath = ref.substring(idx);
+ }
+ } else {
+ mySchemaId = null;
+ myRelativePath = ref;
+ }
+ }
+
+ public boolean isAbsolute() {
+ return mySchemaId != null;
+ }
+
+ @Nullable
+ public String getSchemaId() {
+ return mySchemaId;
+ }
+
+ @NotNull
+ public String getRelativePath() {
+ return myRelativePath;
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVersion.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVersion.java
new file mode 100644
index 00000000..5b0f5507
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVersion.java
@@ -0,0 +1,60 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.openapi.util.text.StringUtil;
+import kotlin.NotImplementedError;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public enum JsonSchemaVersion {
+ SCHEMA_4,
+ SCHEMA_6,
+ SCHEMA_7;
+
+ private static final String ourSchemaV4Schema = "http://json-schema.org/draft-04/schema";
+ private static final String ourSchemaV6Schema = "http://json-schema.org/draft-06/schema";
+ private static final String ourSchemaV7Schema = "http://json-schema.org/draft-07/schema";
+ private static final String ourSchemaVLatestSchema = "http://json-schema.org/schema";
+
+ @Override
+ public String toString() {
+ switch (this) {
+ case SCHEMA_4:
+ return "JSON schema version 4";
+ case SCHEMA_6:
+ return "JSON schema version 6";
+ case SCHEMA_7:
+ return "JSON schema version 7";
+ }
+
+ throw new NotImplementedError("Unknown version: " + this);
+ }
+
+
+ @Nullable
+ public static JsonSchemaVersion byId(@NotNull String id) {
+ switch (StringUtil.trimEnd(id, '#')) {
+ case ourSchemaV4Schema:
+ return SCHEMA_4;
+ case ourSchemaV6Schema:
+ return SCHEMA_6;
+ case ourSchemaV7Schema:
+ case ourSchemaVLatestSchema:
+ return SCHEMA_7;
+ }
+
+ return null;
+ }
+
+ public static boolean isSchemaSchemaId(@Nullable String id) {
+ if (id == null) return false;
+ switch (StringUtil.trimEnd(id, '#')) {
+ case ourSchemaV4Schema:
+ case ourSchemaV6Schema:
+ case ourSchemaV7Schema:
+ case ourSchemaVLatestSchema:
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonValidationError.java b/json/src/com/jetbrains/jsonSchema/impl/JsonValidationError.java
new file mode 100644
index 00000000..fa13577c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/JsonValidationError.java
@@ -0,0 +1,163 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl;
+
+import com.intellij.codeInspection.LocalQuickFix;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.impl.fixes.AddMissingPropertyFix;
+import com.jetbrains.jsonSchema.impl.fixes.RemoveProhibitedPropertyFix;
+import com.jetbrains.jsonSchema.impl.fixes.SuggestEnumValuesFix;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.stream.Collectors;
+
+public class JsonValidationError {
+
+ public IssueData getIssueData() {
+ return myIssueData;
+ }
+
+ public JsonErrorPriority getPriority() {
+ return myPriority;
+ }
+
+ public enum FixableIssueKind {
+ MissingProperty,
+ MissingOneOfProperty,
+ MissingAnyOfProperty,
+ ProhibitedProperty,
+ NonEnumValue,
+ ProhibitedType,
+ TypeMismatch,
+ None
+ }
+
+ public interface IssueData {
+
+ }
+
+ public static class MissingOneOfPropsIssueData implements IssueData {
+ public final Collection<MissingMultiplePropsIssueData> myExclusiveOptions;
+
+ public MissingOneOfPropsIssueData(Collection<MissingMultiplePropsIssueData> options) {
+ myExclusiveOptions = options;
+ }
+ }
+
+ public static class MissingMultiplePropsIssueData implements IssueData {
+ public final Collection<MissingPropertyIssueData> myMissingPropertyIssues;
+
+ public MissingMultiplePropsIssueData(Collection<MissingPropertyIssueData> missingPropertyIssues) {
+ myMissingPropertyIssues = missingPropertyIssues;
+ }
+
+ private static String getPropertyNameWithComment(MissingPropertyIssueData prop) {
+ String comment = "";
+ if (prop.enumItemsCount == 1) {
+ comment = " = " + prop.defaultValue.toString();
+ }
+ return "'" + prop.propertyName + "'" + comment;
+ }
+
+ public String getMessage(boolean trimIfNeeded) {
+ if (myMissingPropertyIssues.size() == 1) {
+ MissingPropertyIssueData prop = myMissingPropertyIssues.iterator().next();
+ return "property " + getPropertyNameWithComment(prop);
+ }
+
+ Collection<MissingPropertyIssueData> namesToDisplay = myMissingPropertyIssues;
+ boolean trimmed = false;
+ if (trimIfNeeded && namesToDisplay.size() > 3) {
+ namesToDisplay = ContainerUtil.newArrayList();
+ Iterator<MissingPropertyIssueData> iterator = myMissingPropertyIssues.iterator();
+ for (int i = 0; i < 3; i++) {
+ namesToDisplay.add(iterator.next());
+ }
+ trimmed = true;
+ }
+ String allNames = myMissingPropertyIssues.stream().map(
+ MissingMultiplePropsIssueData::getPropertyNameWithComment).sorted((s1, s2) -> {
+ boolean firstHasEq = s1.contains("=");
+ boolean secondHasEq = s2.contains("=");
+ if (firstHasEq == secondHasEq) {
+ return s1.compareTo(s2);
+ }
+ return firstHasEq ? -1 : 1;
+ }).collect(Collectors.joining(", "));
+ if (trimmed) allNames += ", ...";
+ return "properties " + allNames;
+ }
+ }
+
+ public static class MissingPropertyIssueData implements IssueData {
+ public final String propertyName;
+ public final JsonSchemaType propertyType;
+ public final Object defaultValue;
+ public final int enumItemsCount;
+
+ public MissingPropertyIssueData(String propertyName, JsonSchemaType propertyType, Object defaultValue, int enumItemsCount) {
+ this.propertyName = propertyName;
+ this.propertyType = propertyType;
+ this.defaultValue = defaultValue;
+ this.enumItemsCount = enumItemsCount;
+ }
+ }
+
+ public static class ProhibitedPropertyIssueData implements IssueData {
+ public final String propertyName;
+
+ public ProhibitedPropertyIssueData(String propertyName) {
+ this.propertyName = propertyName;
+ }
+ }
+
+ public static class TypeMismatchIssueData implements IssueData {
+ public final JsonSchemaType[] expectedTypes;
+
+ public TypeMismatchIssueData(JsonSchemaType[] expectedTypes) {
+ this.expectedTypes = expectedTypes;
+ }
+ }
+
+ private final String myMessage;
+ private final FixableIssueKind myFixableIssueKind;
+ private final IssueData myIssueData;
+ private final JsonErrorPriority myPriority;
+
+ public JsonValidationError(String message, FixableIssueKind fixableIssueKind, IssueData issueData,
+ JsonErrorPriority priority) {
+ myMessage = message;
+ myFixableIssueKind = fixableIssueKind;
+ myIssueData = issueData;
+ myPriority = priority;
+ }
+
+ public String getMessage() {
+ return myMessage;
+ }
+
+ public FixableIssueKind getFixableIssueKind() {
+ return myFixableIssueKind;
+ }
+
+ @NotNull
+ public LocalQuickFix[] createFixes(@Nullable JsonLikePsiWalker.QuickFixAdapter quickFixAdapter) {
+ if (quickFixAdapter == null) return LocalQuickFix.EMPTY_ARRAY;
+ switch (myFixableIssueKind) {
+ case MissingProperty:
+ return new AddMissingPropertyFix[]{new AddMissingPropertyFix((MissingMultiplePropsIssueData)myIssueData, quickFixAdapter)};
+ case MissingOneOfProperty:
+ case MissingAnyOfProperty:
+ return ((MissingOneOfPropsIssueData)myIssueData).myExclusiveOptions.stream().map(d -> new AddMissingPropertyFix(d, quickFixAdapter)).toArray(LocalQuickFix[]::new);
+ case ProhibitedProperty:
+ return new RemoveProhibitedPropertyFix[]{new RemoveProhibitedPropertyFix((ProhibitedPropertyIssueData)myIssueData, quickFixAdapter)};
+ case NonEnumValue:
+ return new SuggestEnumValuesFix[]{new SuggestEnumValuesFix(quickFixAdapter)};
+ default:
+ return LocalQuickFix.EMPTY_ARRAY;
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/MatchResult.java b/json/src/com/jetbrains/jsonSchema/impl/MatchResult.java
new file mode 100644
index 00000000..910313e7
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/MatchResult.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2000-2017 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.util.Processor;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.containers.MultiMap;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+
+/**
+ * @author Irina.Chernushina on 4/22/2017.
+ */
+public class MatchResult {
+ public final List<JsonSchemaObject> mySchemas;
+ public final List<Collection<? extends JsonSchemaObject>> myExcludingSchemas;
+
+ private MatchResult(@NotNull final List<JsonSchemaObject> schemas, @NotNull final List<Collection<? extends JsonSchemaObject>> excludingSchemas) {
+ mySchemas = Collections.unmodifiableList(schemas);
+ myExcludingSchemas = Collections.unmodifiableList(excludingSchemas);
+ }
+
+ public static MatchResult create(@NotNull JsonSchemaTreeNode root) {
+ List<JsonSchemaObject> schemas = new ArrayList<>();
+ MultiMap<Integer, JsonSchemaObject> oneOfGroups = MultiMap.create();
+ iterateTree(root, node -> {
+ if (node.isAny()) return true;
+ int groupNumber = node.getExcludingGroupNumber();
+ if (groupNumber < 0) {
+ schemas.add(node.getSchema());
+ }
+ else {
+ oneOfGroups.putValue(groupNumber, node.getSchema());
+ }
+ return true;
+ });
+ List<Collection<? extends JsonSchemaObject>> result = oneOfGroups.isEmpty()
+ ? ContainerUtil.emptyList()
+ : ContainerUtil.newArrayListWithCapacity(oneOfGroups.keySet().size());
+ for (Map.Entry<Integer, Collection<JsonSchemaObject>> entry: oneOfGroups.entrySet()) {
+ result.add(entry.getValue());
+ }
+ return new MatchResult(schemas, result);
+ }
+
+ public static void iterateTree(@NotNull JsonSchemaTreeNode root,
+ @NotNull final Processor<? super JsonSchemaTreeNode> processor) {
+ final ArrayDeque<JsonSchemaTreeNode> queue = new ArrayDeque<>(root.getChildren());
+ while (!queue.isEmpty()) {
+ final JsonSchemaTreeNode node = queue.removeFirst();
+ if (node.getChildren().isEmpty()) {
+ if (!node.isNothing() && SchemaResolveState.normal.equals(node.getResolveState()) && !processor.process(node)) {
+ break;
+ }
+ } else {
+ queue.addAll(node.getChildren());
+ }
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/SchemaResolveState.java b/json/src/com/jetbrains/jsonSchema/impl/SchemaResolveState.java
new file mode 100644
index 00000000..2c83bcb2
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/SchemaResolveState.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2000-2017 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;
+
+/**
+ * @author Irina.Chernushina on 4/24/2017.
+ */
+enum SchemaResolveState {
+ normal, conflict, brokenDefinition, cyclicDefinition
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonArrayAdapter.java b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonArrayAdapter.java
new file mode 100644
index 00000000..5ad2927a
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonArrayAdapter.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2000-2017 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.adapters;
+
+import com.intellij.json.psi.JsonArray;
+import com.intellij.psi.PsiElement;
+import com.jetbrains.jsonSchema.extension.adapters.JsonArrayValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public class JsonJsonArrayAdapter implements JsonArrayValueAdapter {
+ @NotNull private final JsonArray myArray;
+
+ public JsonJsonArrayAdapter(@NotNull JsonArray array) {myArray = array;}
+
+ @Override
+ public boolean isObject() {
+ return false;
+ }
+
+ @Override
+ public boolean isArray() {
+ return true;
+ }
+
+ @Override
+ public boolean isStringLiteral() {
+ return false;
+ }
+
+ @Override
+ public boolean isNumberLiteral() {
+ return false;
+ }
+
+ @Override
+ public boolean isBooleanLiteral() {
+ return false;
+ }
+
+ @NotNull
+ @Override
+ public PsiElement getDelegate() {
+ return myArray;
+ }
+
+ @Nullable
+ @Override
+ public JsonObjectValueAdapter getAsObject() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public JsonArrayValueAdapter getAsArray() {
+ return this;
+ }
+
+ @NotNull
+ @Override
+ public List<JsonValueAdapter> getElements() {
+ return myArray.getValueList().stream().filter(e -> e != null).map(e -> JsonJsonPropertyAdapter.createAdapterByType(e)).collect(
+ Collectors.toList());
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonGenericValueAdapter.java b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonGenericValueAdapter.java
new file mode 100644
index 00000000..fe84e7eb
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonGenericValueAdapter.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2000-2017 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.adapters;
+
+import com.intellij.json.psi.*;
+import com.intellij.psi.PsiElement;
+import com.jetbrains.jsonSchema.extension.adapters.JsonArrayValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public class JsonJsonGenericValueAdapter implements JsonValueAdapter {
+ @NotNull private final JsonValue myValue;
+
+ public JsonJsonGenericValueAdapter(@NotNull JsonValue value) {myValue = value;}
+
+ @Override
+ public boolean isObject() {
+ return false;
+ }
+
+ @Override
+ public boolean isArray() {
+ return false;
+ }
+
+ @Override
+ public boolean isStringLiteral() {
+ return myValue instanceof JsonStringLiteral;
+ }
+
+ @Override
+ public boolean isNumberLiteral() {
+ return myValue instanceof JsonNumberLiteral;
+ }
+
+ @Override
+ public boolean isBooleanLiteral() {
+ return myValue instanceof JsonBooleanLiteral;
+ }
+
+ @Override
+ public boolean isNull() {
+ return myValue instanceof JsonNullLiteral;
+ }
+
+ @NotNull
+ @Override
+ public PsiElement getDelegate() {
+ return myValue;
+ }
+
+ @Nullable
+ @Override
+ public JsonObjectValueAdapter getAsObject() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public JsonArrayValueAdapter getAsArray() {
+ return null;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonObjectAdapter.java b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonObjectAdapter.java
new file mode 100644
index 00000000..db19cd1c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonObjectAdapter.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2000-2017 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.adapters;
+
+import com.intellij.json.psi.JsonObject;
+import com.intellij.psi.PsiElement;
+import com.jetbrains.jsonSchema.extension.adapters.JsonArrayValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public class JsonJsonObjectAdapter implements JsonObjectValueAdapter {
+ @NotNull private final JsonObject myValue;
+
+ public JsonJsonObjectAdapter(@NotNull JsonObject value) {myValue = value;}
+
+ @Override
+ public boolean isObject() {
+ return true;
+ }
+
+ @Override
+ public boolean isArray() {
+ return false;
+ }
+
+ @Override
+ public boolean isStringLiteral() {
+ return false;
+ }
+
+ @Override
+ public boolean isNumberLiteral() {
+ return false;
+ }
+
+ @Override
+ public boolean isBooleanLiteral() {
+ return false;
+ }
+
+ @NotNull
+ @Override
+ public PsiElement getDelegate() {
+ return myValue;
+ }
+
+ @Nullable
+ @Override
+ public JsonObjectValueAdapter getAsObject() {
+ return this;
+ }
+
+ @Nullable
+ @Override
+ public JsonArrayValueAdapter getAsArray() {
+ return null;
+ }
+
+ @NotNull
+ @Override
+ public List<JsonPropertyAdapter> getPropertyList() {
+ return myValue.getPropertyList().stream().filter(p -> p != null)
+ .map(p -> new JsonJsonPropertyAdapter(p)).collect(Collectors.toList());
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonPropertyAdapter.java b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonPropertyAdapter.java
new file mode 100644
index 00000000..f3871c35
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonPropertyAdapter.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2000-2017 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.adapters;
+
+import com.intellij.json.psi.JsonArray;
+import com.intellij.json.psi.JsonObject;
+import com.intellij.json.psi.JsonProperty;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
+import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * @author Irina.Chernushina on 2/20/2017.
+ */
+public class JsonJsonPropertyAdapter implements JsonPropertyAdapter {
+ @NotNull private final JsonProperty myProperty;
+
+ public JsonJsonPropertyAdapter(@NotNull JsonProperty property) {
+ myProperty = property;
+ }
+
+ @Nullable
+ @Override
+ public String getName() {
+ return myProperty.getName();
+ }
+
+ @NotNull
+ @Override
+ public Collection<JsonValueAdapter> getValues() {
+ return myProperty.getValue() == null ? ContainerUtil.emptyList() : Collections.singletonList(createAdapterByType(myProperty.getValue()));
+ }
+
+ @Nullable
+ @Override
+ public JsonValueAdapter getNameValueAdapter() {
+ return createAdapterByType(myProperty.getNameElement());
+ }
+
+ @NotNull
+ @Override
+ public PsiElement getDelegate() {
+ return myProperty;
+ }
+
+ @Nullable
+ @Override
+ public JsonObjectValueAdapter getParentObject() {
+ return myProperty.getParent() instanceof JsonObject ? new JsonJsonObjectAdapter((JsonObject)myProperty.getParent()) : null;
+ }
+
+ @NotNull
+ public static JsonValueAdapter createAdapterByType(@NotNull JsonValue value) {
+ if (value instanceof JsonObject) return new JsonJsonObjectAdapter((JsonObject)value);
+ if (value instanceof JsonArray) return new JsonJsonArrayAdapter((JsonArray)value);
+ return new JsonJsonGenericValueAdapter(value);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/fixes/AddMissingPropertyFix.java b/json/src/com/jetbrains/jsonSchema/impl/fixes/AddMissingPropertyFix.java
new file mode 100644
index 00000000..8dbcd601
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/fixes/AddMissingPropertyFix.java
@@ -0,0 +1,156 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl.fixes;
+
+import com.intellij.codeInsight.template.Template;
+import com.intellij.codeInsight.template.TemplateBuilderImpl;
+import com.intellij.codeInsight.template.TemplateManager;
+import com.intellij.codeInsight.template.impl.ConstantNode;
+import com.intellij.codeInsight.template.impl.EmptyNode;
+import com.intellij.codeInsight.template.impl.MacroCallNode;
+import com.intellij.codeInsight.template.macro.CompleteMacro;
+import com.intellij.codeInspection.*;
+import com.intellij.openapi.application.WriteAction;
+import com.intellij.openapi.editor.ex.EditorEx;
+import com.intellij.openapi.editor.ex.util.EditorUtil;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.DocumentUtil;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.impl.JsonSchemaType;
+import com.jetbrains.jsonSchema.impl.JsonValidationError;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class AddMissingPropertyFix implements LocalQuickFix, BatchQuickFix<CommonProblemDescriptor> {
+ private final JsonValidationError.MissingMultiplePropsIssueData myData;
+ private final JsonLikePsiWalker.QuickFixAdapter myQuickFixAdapter;
+
+ public AddMissingPropertyFix(JsonValidationError.MissingMultiplePropsIssueData data,
+ JsonLikePsiWalker.QuickFixAdapter quickFixAdapter) {
+ myData = data;
+ myQuickFixAdapter = quickFixAdapter;
+ }
+
+ @Nls(capitalization = Nls.Capitalization.Sentence)
+ @NotNull
+ @Override
+ public String getFamilyName() {
+ return "Add missing properties";
+ }
+
+ @Nls(capitalization = Nls.Capitalization.Sentence)
+ @NotNull
+ @Override
+ public String getName() {
+ return "Add missing " + myData.getMessage(true);
+ }
+
+ @Override
+ public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
+ PsiElement element = descriptor.getPsiElement();
+ Ref<Boolean> hadComma = Ref.create(false);
+ PsiElement newElement = performFix(element, hadComma);
+ // if we have more than one property, don't expand templates and don't move the caret
+ if (newElement == null) return;
+
+ PsiElement value = myQuickFixAdapter.getPropertyValue(newElement);
+ FileEditor fileEditor = FileEditorManager.getInstance(project).getSelectedEditor(element.getContainingFile().getVirtualFile());
+ EditorEx editor = EditorUtil.getEditorEx(fileEditor);
+ assert editor != null;
+ if (value == null) {
+ WriteAction.run(() -> editor.getCaretModel().moveToOffset(newElement.getTextRange().getEndOffset()));
+ return;
+ }
+ TemplateManager templateManager = TemplateManager.getInstance(project);
+ TemplateBuilderImpl builder = new TemplateBuilderImpl(newElement);
+ String text = value.getText();
+ boolean isEmptyArray = StringUtil.equalsIgnoreWhitespaces(text, "[]");
+ boolean isEmptyObject = StringUtil.equalsIgnoreWhitespaces(text, "{}");
+ boolean goInside = isEmptyArray || isEmptyObject || StringUtil.isQuotedString(text);
+ TextRange range = goInside ? TextRange.create(1, text.length() - 1) : TextRange.create(0, text.length());
+ builder.replaceElement(value, range, myData.myMissingPropertyIssues.iterator().next().enumItemsCount > 1 || isEmptyObject
+ ? new MacroCallNode(new CompleteMacro())
+ : isEmptyArray ? new EmptyNode() : new ConstantNode(goInside ? StringUtil.unquoteString(text) : text));
+ editor.getCaretModel().moveToOffset(newElement.getTextRange().getStartOffset());
+ builder.setEndVariableAfter(newElement);
+ WriteAction.run(() -> {
+ Template template = builder.buildInlineTemplate();
+ template.setToReformat(true);
+ templateManager.startTemplate(editor, template);
+ });
+ }
+
+ private PsiElement performFix(PsiElement element, Ref<Boolean> hadComma) {
+ Ref<PsiElement> newElementRef = Ref.create(null);
+
+ WriteAction.run(() -> {
+ boolean isSingle = myData.myMissingPropertyIssues.size() == 1;
+ for (JsonValidationError.MissingPropertyIssueData issue: myData.myMissingPropertyIssues) {
+ Object defaultValueObject = issue.defaultValue;
+ String defaultValue = defaultValueObject instanceof String ? StringUtil.wrapWithDoubleQuote(defaultValueObject.toString()) : null;
+ PsiElement newElement = element
+ .addBefore(
+ myQuickFixAdapter.createProperty(issue.propertyName, defaultValue == null ? getDefaultValueFromType(issue) : defaultValue),
+ element.getLastChild());
+ PsiElement backward = PsiTreeUtil.skipWhitespacesBackward(newElement);
+ hadComma.set(myQuickFixAdapter.ensureComma(backward, element, newElement));
+ if (isSingle) {
+ newElementRef.set(newElement);
+ }
+ }
+ });
+
+ return newElementRef.get();
+ }
+
+ @NotNull
+ private static String getDefaultValueFromType(JsonValidationError.MissingPropertyIssueData issue) {
+ JsonSchemaType propertyType = issue.propertyType;
+ return propertyType == null ? "" : propertyType.getDefaultValue();
+ }
+
+ @Override
+ public boolean startInWriteAction() {
+ return false;
+ }
+
+ @Override
+ public void applyFix(@NotNull Project project,
+ @NotNull CommonProblemDescriptor[] descriptors,
+ @NotNull List<PsiElement> psiElementsToIgnore,
+ @Nullable Runnable refreshViews) {
+ List<Pair<AddMissingPropertyFix, PsiElement>> propFixes = ContainerUtil.newArrayList();
+ for (CommonProblemDescriptor descriptor: descriptors) {
+ if (!(descriptor instanceof ProblemDescriptor)) continue;
+ QuickFix[] fixes = descriptor.getFixes();
+ if (fixes == null) continue;
+ AddMissingPropertyFix fix = getWorkingQuickFix(fixes);
+ if (fix == null) continue;
+ propFixes.add(Pair.create(fix, ((ProblemDescriptor)descriptor).getPsiElement()));
+ }
+
+ DocumentUtil.writeInRunUndoTransparentAction(() -> propFixes.forEach(fix ->
+ fix.first.performFix(fix.second, Ref.create(false))));
+ }
+
+ @Nullable
+ private static AddMissingPropertyFix getWorkingQuickFix(@NotNull QuickFix[] fixes) {
+ for (QuickFix fix : fixes) {
+ if (fix instanceof AddMissingPropertyFix) {
+ return (AddMissingPropertyFix)fix;
+ }
+ }
+ return null;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/fixes/RemoveProhibitedPropertyFix.java b/json/src/com/jetbrains/jsonSchema/impl/fixes/RemoveProhibitedPropertyFix.java
new file mode 100644
index 00000000..fb211e4c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/fixes/RemoveProhibitedPropertyFix.java
@@ -0,0 +1,46 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl.fixes;
+
+import com.intellij.codeInspection.LocalQuickFix;
+import com.intellij.codeInspection.ProblemDescriptor;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.impl.JsonValidationError;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+
+public class RemoveProhibitedPropertyFix implements LocalQuickFix {
+ private final JsonValidationError.ProhibitedPropertyIssueData myData;
+ private final JsonLikePsiWalker.QuickFixAdapter myQuickFixAdapter;
+
+ public RemoveProhibitedPropertyFix(JsonValidationError.ProhibitedPropertyIssueData data,
+ JsonLikePsiWalker.QuickFixAdapter quickFixAdapter) {
+ myData = data;
+ myQuickFixAdapter = quickFixAdapter;
+ }
+
+ @Nls(capitalization = Nls.Capitalization.Sentence)
+ @NotNull
+ @Override
+ public String getFamilyName() {
+ return "Remove prohibited property";
+ }
+
+ @Nls(capitalization = Nls.Capitalization.Sentence)
+ @NotNull
+ @Override
+ public String getName() {
+ return getFamilyName() + " '" + myData.propertyName + "'";
+ }
+
+ @Override
+ public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
+ PsiElement element = descriptor.getPsiElement();
+ assert myData.propertyName.equals(myQuickFixAdapter.getPropertyName(element));
+ PsiElement forward = PsiTreeUtil.skipWhitespacesForward(element);
+ element.delete();
+ myQuickFixAdapter.removeIfComma(forward);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/fixes/SuggestEnumValuesFix.java b/json/src/com/jetbrains/jsonSchema/impl/fixes/SuggestEnumValuesFix.java
new file mode 100644
index 00000000..3d00f1f2
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/fixes/SuggestEnumValuesFix.java
@@ -0,0 +1,89 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl.fixes;
+
+import com.intellij.codeInsight.completion.CodeCompletionHandlerBase;
+import com.intellij.codeInsight.completion.CompletionType;
+import com.intellij.codeInsight.hint.HintManager;
+import com.intellij.codeInspection.BatchQuickFix;
+import com.intellij.codeInspection.CommonProblemDescriptor;
+import com.intellij.codeInspection.LocalQuickFix;
+import com.intellij.codeInspection.ProblemDescriptor;
+import com.intellij.openapi.application.WriteAction;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.ex.EditorEx;
+import com.intellij.openapi.editor.ex.util.EditorUtil;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiWhiteSpace;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class SuggestEnumValuesFix implements LocalQuickFix, BatchQuickFix<CommonProblemDescriptor> {
+ private final JsonLikePsiWalker.QuickFixAdapter myQuickFixAdapter;
+
+ public SuggestEnumValuesFix(JsonLikePsiWalker.QuickFixAdapter quickFixAdapter) {
+ myQuickFixAdapter = quickFixAdapter;
+ }
+
+ @Nls(capitalization = Nls.Capitalization.Sentence)
+ @NotNull
+ @Override
+ public String getFamilyName() {
+ return "Replace with allowed value";
+ }
+
+ @Nls(capitalization = Nls.Capitalization.Sentence)
+ @NotNull
+ @Override
+ public String getName() {
+ return getFamilyName();
+ }
+
+ @Override
+ public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
+ PsiElement initialElement = descriptor.getPsiElement();
+ PsiElement element = myQuickFixAdapter.adjustValue(initialElement);
+ FileEditor fileEditor = FileEditorManager.getInstance(project).getSelectedEditor(element.getContainingFile().getVirtualFile());
+ boolean whitespaceBefore = false;
+ if (element.getPrevSibling() instanceof PsiWhiteSpace) {
+ whitespaceBefore = true;
+ }
+ WriteAction.run(() -> element.delete());
+ EditorEx editor = EditorUtil.getEditorEx(fileEditor);
+ assert editor != null;
+ if (myQuickFixAdapter.fixWhitespaceBefore(initialElement, element) && whitespaceBefore) {
+ WriteAction.run(() -> {
+ int offset = editor.getCaretModel().getOffset();
+ editor.getDocument().insertString(offset, " ");
+ editor.getCaretModel().moveToOffset(offset + 1);
+ });
+ }
+ CodeCompletionHandlerBase.createHandler(CompletionType.BASIC).invokeCompletion(project, editor);
+ }
+
+ @Override
+ public boolean startInWriteAction() {
+ return false;
+ }
+
+ @Override
+ public void applyFix(@NotNull Project project,
+ @NotNull CommonProblemDescriptor[] descriptors,
+ @NotNull List<PsiElement> psiElementsToIgnore,
+ @Nullable Runnable refreshViews) {
+ Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
+ if (editor != null) {
+ HintManager.getInstance().showErrorHint(editor, "Sorry, this fix is not available in batch mode");
+ }
+ else {
+ Messages.showErrorDialog(project, "Sorry, this fix is not available in batch mode", "Not Applicable in Batch Mode");
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaBasedInspectionBase.java b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaBasedInspectionBase.java
new file mode 100644
index 00000000..0a47e699
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaBasedInspectionBase.java
@@ -0,0 +1,46 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl.inspections;
+
+import com.intellij.codeHighlighting.HighlightDisplayLevel;
+import com.intellij.codeInspection.LocalInspectionTool;
+import com.intellij.codeInspection.LocalInspectionToolSession;
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.json.psi.JsonFile;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.PsiFile;
+import com.intellij.util.ObjectUtils;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public abstract class JsonSchemaBasedInspectionBase extends LocalInspectionTool {
+ @NotNull
+ @Override
+ public HighlightDisplayLevel getDefaultLevel() {
+ return HighlightDisplayLevel.WARNING;
+ }
+
+ @NotNull
+ @Override
+ public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly, @NotNull LocalInspectionToolSession session) {
+ PsiFile file = holder.getFile();
+ JsonValue root = file instanceof JsonFile ? ObjectUtils.tryCast(file.getFirstChild(), JsonValue.class) : null;
+ if (root == null) return PsiElementVisitor.EMPTY_VISITOR;
+
+ JsonSchemaService service = JsonSchemaService.Impl.get(file.getProject());
+ VirtualFile virtualFile = file.getViewProvider().getVirtualFile();
+ if (!service.isApplicableToFile(virtualFile)) return PsiElementVisitor.EMPTY_VISITOR;
+ final JsonSchemaObject rootSchema = service.getSchemaObject(virtualFile);
+
+ return doBuildVisitor(root, rootSchema, service, holder, session);
+ }
+
+ protected abstract PsiElementVisitor doBuildVisitor(@NotNull JsonValue root,
+ @Nullable JsonSchemaObject schema,
+ @NotNull JsonSchemaService service,
+ @NotNull ProblemsHolder holder,
+ @NotNull LocalInspectionToolSession session);
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaComplianceInspection.java b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaComplianceInspection.java
new file mode 100644
index 00000000..0ff94732
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaComplianceInspection.java
@@ -0,0 +1,67 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl.inspections;
+
+import com.intellij.codeInspection.LocalInspectionToolSession;
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.codeInspection.ui.MultipleCheckboxOptionsPanel;
+import com.intellij.json.JsonBundle;
+import com.intellij.json.psi.JsonElementVisitor;
+import com.intellij.json.psi.JsonValue;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonComplianceCheckerOptions;
+import com.jetbrains.jsonSchema.impl.JsonSchemaComplianceChecker;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+public class JsonSchemaComplianceInspection extends JsonSchemaBasedInspectionBase {
+ public boolean myCaseInsensitiveEnum = false;
+
+ @Override
+ @NotNull
+ public String getDisplayName() {
+ return JsonBundle.message("json.schema.inspection.compliance.name");
+ }
+
+ @Override
+ protected PsiElementVisitor doBuildVisitor(@NotNull JsonValue root, @Nullable JsonSchemaObject schema, @NotNull JsonSchemaService service,
+ @NotNull ProblemsHolder holder,
+ @NotNull LocalInspectionToolSession session) {
+ if (schema == null) return PsiElementVisitor.EMPTY_VISITOR;
+ JsonComplianceCheckerOptions options = new JsonComplianceCheckerOptions(myCaseInsensitiveEnum);
+
+ return new JsonElementVisitor() {
+ @Override
+ public void visitElement(PsiElement element) {
+ if (element == root) {
+ // perform this only for the root element, because the checker traverses the hierarchy itself
+ annotate(element, schema, holder, session, options);
+ }
+ super.visitElement(element);
+ }
+ };
+ }
+
+ @Nullable
+ @Override
+ public JComponent createOptionsPanel() {
+ final MultipleCheckboxOptionsPanel optionsPanel = new MultipleCheckboxOptionsPanel(this);
+ optionsPanel.addCheckbox(JsonBundle.message("json.schema.inspection.case.insensitive.enum"), "myCaseInsensitiveEnum");
+ return optionsPanel;
+ }
+
+ private static void annotate(@NotNull PsiElement element,
+ @NotNull JsonSchemaObject rootSchema,
+ @NotNull ProblemsHolder holder,
+ @NotNull LocalInspectionToolSession session,
+ JsonComplianceCheckerOptions options) {
+ final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(element, rootSchema);
+ if (walker == null) return;
+ new JsonSchemaComplianceChecker(rootSchema, holder, walker, session, options).annotate(element);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaRefReferenceInspection.java b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaRefReferenceInspection.java
new file mode 100644
index 00000000..395b9429
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaRefReferenceInspection.java
@@ -0,0 +1,93 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.impl.inspections;
+
+import com.intellij.codeInspection.LocalInspectionToolSession;
+import com.intellij.codeInspection.ProblemHighlightType;
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.json.JsonBundle;
+import com.intellij.json.psi.*;
+import com.intellij.openapi.paths.WebReference;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonPointerReferenceProvider;
+import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class JsonSchemaRefReferenceInspection extends JsonSchemaBasedInspectionBase {
+ @Override
+ @NotNull
+ public String getDisplayName() {
+ return JsonBundle.message("json.schema.ref.refs.inspection.name");
+ }
+
+ @Override
+ protected PsiElementVisitor doBuildVisitor(@NotNull JsonValue root,
+ @Nullable JsonSchemaObject schema,
+ @NotNull JsonSchemaService service,
+ @NotNull ProblemsHolder holder,
+ @NotNull LocalInspectionToolSession session) {
+ boolean checkRefs = schema != null && service.isSchemaFile(schema.getSchemaFile());
+ return new JsonElementVisitor() {
+ @Override
+ public void visitElement(PsiElement element) {
+ if (element == root) {
+ if (element instanceof JsonObject) {
+ final JsonProperty schemaProp = ((JsonObject)element).findProperty("$schema");
+ if (schemaProp != null) {
+ doCheck(schemaProp.getValue());
+ }
+ }
+ }
+ super.visitElement(element);
+ }
+
+ @Override
+ public void visitProperty(@NotNull JsonProperty o) {
+ if (!checkRefs) return;
+ if ("$ref".equals(o.getName())) {
+ doCheck(o.getValue());
+ }
+ super.visitProperty(o);
+ }
+
+ private void doCheck(JsonValue value) {
+ if (!(value instanceof JsonStringLiteral)) return;
+ for (PsiReference reference : value.getReferences()) {
+ if (reference instanceof WebReference) continue;
+ final PsiElement resolved = reference.resolve();
+ if (resolved == null) {
+ holder.registerProblem(reference, getReferenceErrorDesc(reference), ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
+ }
+ }
+ }
+
+ private String getReferenceErrorDesc(PsiReference reference) {
+ final String text = reference.getCanonicalText();
+ if (reference instanceof FileReference) {
+ final int hash = text.indexOf('#');
+ return JsonBundle.message("json.schema.ref.file.not.found", hash == -1 ? text : text.substring(0, hash));
+ }
+ if (reference instanceof JsonPointerReferenceProvider.JsonSchemaIdReference) {
+ return JsonBundle.message("json.schema.ref.cannot.resolve.id", text);
+ }
+ final int lastSlash = text.lastIndexOf('/');
+ if (lastSlash == -1) {
+ return JsonBundle.message("json.schema.ref.cannot.resolve.path", text);
+ }
+ final String substring = text.substring(text.lastIndexOf('/') + 1);
+
+ try {
+ Integer.parseInt(substring);
+ return JsonBundle.message("json.schema.ref.no.array.element", substring);
+ }
+ catch (Exception e) {
+ return JsonBundle.message("json.schema.ref.no.property", substring);
+ }
+ }
+ };
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/remote/JsonFileResolver.java b/json/src/com/jetbrains/jsonSchema/remote/JsonFileResolver.java
new file mode 100644
index 00000000..76cbff16
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/remote/JsonFileResolver.java
@@ -0,0 +1,92 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.remote;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Couple;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
+import com.intellij.openapi.vfs.impl.http.RemoteFileInfo;
+import com.intellij.openapi.vfs.impl.http.RemoteFileState;
+import com.intellij.util.UriUtil;
+import com.intellij.util.Url;
+import com.intellij.util.Urls;
+import com.jetbrains.jsonSchema.JsonSchemaCatalogProjectConfiguration;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+
+public class JsonFileResolver {
+ public static boolean isRemoteEnabled(Project project) {
+ return !ApplicationManager.getApplication().isUnitTestMode() &&
+ JsonSchemaCatalogProjectConfiguration.getInstance(project).isRemoteActivityEnabled();
+ }
+
+ @Nullable
+ public static VirtualFile urlToFile(@NotNull String urlString) {
+ return VirtualFileManager.getInstance().findFileByUrl(replaceUnsafeSchemaStoreUrls(urlString));
+ }
+
+ @Nullable
+ @Contract("null -> null; !null -> !null")
+ public static String replaceUnsafeSchemaStoreUrls(@Nullable String urlString) {
+ if (urlString == null) return null;
+ if (urlString.equals(JsonSchemaCatalogManager.DEFAULT_CATALOG)) {
+ return JsonSchemaCatalogManager.DEFAULT_CATALOG_HTTPS;
+ }
+ if (StringUtil.startsWithIgnoreCase(urlString, JsonSchemaRemoteContentProvider.STORE_URL_PREFIX_HTTP)) {
+ String newUrl = StringUtil.replace(urlString, "http://json.schemastore.org/", "https://schemastore.azurewebsites.net/schemas/json/");
+ return newUrl.endsWith(".json") ? newUrl : newUrl + ".json";
+ }
+ return urlString;
+ }
+
+ @Nullable
+ public static VirtualFile resolveSchemaByReference(@Nullable VirtualFile currentFile,
+ @Nullable String schemaUrl) {
+ if (schemaUrl == null) return null;
+
+ boolean isHttpPath = isHttpPath(schemaUrl);
+
+ if (StringUtil.startsWithChar(schemaUrl, '.') || !isHttpPath) {
+ // relative path
+ VirtualFile parent = currentFile == null ? null : currentFile.getParent();
+ schemaUrl = parent == null ? null : VfsUtilCore.pathToUrl(parent.getPath() + File.separator + schemaUrl);
+ }
+
+ if (schemaUrl != null) {
+ VirtualFile virtualFile = urlToFile(schemaUrl);
+ // validate the URL before returning the file
+ if (virtualFile instanceof HttpVirtualFile) {
+ String url = virtualFile.getUrl();
+ Url parse = Urls.parse(url, false);
+ if (parse == null || StringUtil.isEmpty(parse.getAuthority()) || StringUtil.isEmpty(parse.getPath())) return null;
+ }
+ if (virtualFile != null) return virtualFile;
+ }
+
+ return null;
+ }
+
+ public static void startFetchingHttpFileIfNeeded(@Nullable VirtualFile path, Project project) {
+ if (!(path instanceof HttpVirtualFile)) return;
+
+ // don't resolve http paths in tests
+ if (!isRemoteEnabled(project)) return;
+
+ RemoteFileInfo info = ((HttpVirtualFile)path).getFileInfo();
+ if (info == null || info.getState() == RemoteFileState.DOWNLOADING_NOT_STARTED) {
+ path.refresh(true, false);
+ }
+ }
+
+ public static boolean isHttpPath(@NotNull String schemaFieldText) {
+ Couple<String> couple = UriUtil.splitScheme(schemaFieldText);
+ return couple.first.startsWith("http");
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogExclusion.java b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogExclusion.java
new file mode 100644
index 00000000..f5ec8fa5
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogExclusion.java
@@ -0,0 +1,19 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.remote;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Disables JSON schema download from schema store for particular files.
+ * Not intended to be used except to suppress JSON schema for JSCS
+ */
+@ApiStatus.Experimental
+public interface JsonSchemaCatalogExclusion {
+
+ ExtensionPointName<JsonSchemaCatalogExclusion> EP_NAME = ExtensionPointName.create("com.intellij.json.catalog.exclusion");
+
+ boolean isExcluded(@NotNull VirtualFile file);
+}
diff --git a/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogManager.java b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogManager.java
new file mode 100644
index 00000000..461e94cc
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogManager.java
@@ -0,0 +1,173 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.remote;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.impl.http.FileDownloadingAdapter;
+import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
+import com.intellij.openapi.vfs.impl.http.RemoteFileInfo;
+import com.intellij.openapi.vfs.impl.http.RemoteFileManager;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.JsonSchemaCatalogProjectConfiguration;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonCachedValues;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+
+public class JsonSchemaCatalogManager {
+ static final String DEFAULT_CATALOG = "http://schemastore.org/api/json/catalog.json";
+ static final String DEFAULT_CATALOG_HTTPS = "https://schemastore.azurewebsites.net/api/json/catalog.json";
+ @NotNull private final Project myProject;
+ @NotNull private final JsonSchemaRemoteContentProvider myRemoteContentProvider;
+ @Nullable private VirtualFile myCatalog = null;
+ @NotNull private final ConcurrentMap<String, String> myResolvedMappings = ContainerUtil.newConcurrentMap();
+ private static final String NO_CACHE = "$_$_WS_NO_CACHE_$_$";
+ private static final String EMPTY = "$_$_WS_EMPTY_$_$";
+
+ public JsonSchemaCatalogManager(@NotNull Project project) {
+ myProject = project;
+ myRemoteContentProvider = new JsonSchemaRemoteContentProvider();
+ }
+
+ public void startUpdates() {
+ JsonSchemaCatalogProjectConfiguration.getInstance(myProject).addChangeHandler(() -> {
+ update();
+ JsonSchemaService.Impl.get(myProject).reset();
+ });
+ RemoteFileManager instance = RemoteFileManager.getInstance();
+ instance.addRemoteContentProvider(myRemoteContentProvider);
+ update();
+ }
+
+ private void update() {
+ // ignore schema catalog when remote activity is disabled (when we're in tests or it is off in settings)
+ myCatalog = !JsonFileResolver.isRemoteEnabled(myProject) ? null : JsonFileResolver.urlToFile(DEFAULT_CATALOG);
+ }
+
+ @Nullable
+ public VirtualFile getSchemaFileForFile(@NotNull VirtualFile file) {
+ if (!JsonSchemaCatalogProjectConfiguration.getInstance(myProject).isCatalogEnabled()) return null;
+ for (JsonSchemaCatalogExclusion exclusion : JsonSchemaCatalogExclusion.EP_NAME.getExtensions()) {
+ if (exclusion.isExcluded(file)) {
+ return null;
+ }
+ }
+
+ String name = file.getName();
+ if (myResolvedMappings.containsKey(name)) {
+ String urlString = myResolvedMappings.get(name);
+ if (EMPTY.equals(urlString)) return null;
+ return JsonFileResolver.resolveSchemaByReference(file, urlString);
+ }
+
+ if (myCatalog != null) {
+ String urlString = resolveSchemaFile(file, myCatalog, myProject);
+ if (NO_CACHE.equals(urlString)) return null;
+ myResolvedMappings.put(name, urlString == null ? EMPTY : urlString);
+ return JsonFileResolver.resolveSchemaByReference(file, urlString);
+ }
+
+ return null;
+ }
+
+ public List<String> getAllCatalogSchemas() {
+ if (myCatalog != null) {
+ List<Pair<Collection<String>, String>> catalog = JsonCachedValues.getSchemaCatalog(myCatalog, myProject);
+ if (catalog == null) return ContainerUtil.emptyList();
+ List<String> results = ContainerUtil.newArrayListWithCapacity(catalog.size());
+ for (Pair<Collection<String>, String> item: catalog) {
+ results.add(item.second);
+ }
+ return results;
+ }
+
+ return ContainerUtil.emptyList();
+ }
+
+ private final Map<Runnable, FileDownloadingAdapter> myDownloadingAdapters = ContainerUtil.createConcurrentWeakMap();
+ public void registerCatalogUpdateCallback(Runnable callback) {
+ if (myCatalog instanceof HttpVirtualFile) {
+ RemoteFileInfo info = ((HttpVirtualFile)myCatalog).getFileInfo();
+ if (info != null) {
+ FileDownloadingAdapter adapter = new FileDownloadingAdapter() {
+ @Override
+ public void fileDownloaded(@NotNull VirtualFile localFile) {
+ callback.run();
+ }
+ };
+ myDownloadingAdapters.put(callback, adapter);
+ info.addDownloadingListener(adapter);
+ }
+ }
+ }
+
+ public void unregisterCatalogUpdateCallback(Runnable callback) {
+ if (!myDownloadingAdapters.containsKey(callback)) return;
+
+ if (myCatalog instanceof HttpVirtualFile) {
+ RemoteFileInfo info = ((HttpVirtualFile)myCatalog).getFileInfo();
+ if (info != null) {
+ info.removeDownloadingListener(myDownloadingAdapters.get(callback));
+ }
+ }
+ }
+
+ public void triggerUpdateCatalog(Project project) {
+ JsonFileResolver.startFetchingHttpFileIfNeeded(myCatalog, project);
+ }
+
+ @Nullable
+ private static String resolveSchemaFile(@NotNull VirtualFile file, @NotNull VirtualFile catalogFile, @NotNull Project project) {
+ JsonFileResolver.startFetchingHttpFileIfNeeded(catalogFile, project);
+
+ List<Pair<Collection<String>, String>> schemaCatalog = JsonCachedValues.getSchemaCatalog(catalogFile, project);
+ if (schemaCatalog == null) return catalogFile instanceof HttpVirtualFile ? NO_CACHE : null;
+ String fileName = file.getName();
+ for (Pair<Collection<String>, String> maskAndPath: schemaCatalog) {
+ if (matches(fileName, maskAndPath.first)) {
+ return maskAndPath.second;
+ }
+ }
+
+ return null;
+ }
+
+ private static boolean matches(@NotNull String fileName, @NotNull Collection<String> masks) {
+ for (String mask: masks) {
+ if (matches(fileName, mask)) return true;
+ }
+ return false;
+ }
+
+ private static boolean matches(@NotNull String fileName, @NotNull String mask) {
+ if (mask.equals(fileName)) return true;
+ int star = mask.indexOf('*');
+
+ // no star - no match
+ if (star == -1) return false;
+
+ // *.foo.json
+ if (star == 0 && fileName.startsWith(mask.substring(1))) {
+ return true;
+ }
+
+ // foobar*
+ if (star == mask.length() - 1 && fileName.endsWith(mask.substring(0, mask.length() - 1))) {
+ return true;
+ }
+
+ String beforeStar = mask.substring(0, star);
+ String afterStar = mask.substring(star + 1);
+
+ if (fileName.startsWith(beforeStar) && fileName.endsWith(afterStar)) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaRemoteContentProvider.java b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaRemoteContentProvider.java
new file mode 100644
index 00000000..dad1766b
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaRemoteContentProvider.java
@@ -0,0 +1,125 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.remote;
+
+import com.intellij.json.JsonFileType;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.impl.http.DefaultRemoteContentProvider;
+import com.intellij.util.Url;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.io.HttpRequests;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URLConnection;
+import java.nio.file.Files;
+import java.time.Duration;
+import java.util.List;
+
+public class JsonSchemaRemoteContentProvider extends DefaultRemoteContentProvider {
+ private static final int DEFAULT_CONNECT_TIMEOUT = 10000;
+ private static final long UPDATE_DELAY = Duration.ofHours(4).toMillis();
+ static final String STORE_URL_PREFIX_HTTP = "http://json.schemastore.org";
+ static final String STORE_URL_PREFIX_HTTPS = "https://schemastore.azurewebsites.net";
+ private static final String SCHEMA_URL_PREFIX = "http://json-schema.org/";
+ private static final String ETAG_HEADER = "ETag";
+ private static final String LAST_MODIFIED_HEADER = "Last-Modified";
+
+ private long myLastUpdateTime = 0;
+
+ @Override
+ public boolean canProvideContent(@NotNull Url url) {
+ String externalForm = url.toExternalForm();
+ return externalForm.startsWith(STORE_URL_PREFIX_HTTP)
+ || externalForm.startsWith(STORE_URL_PREFIX_HTTPS)
+ || externalForm.startsWith(SCHEMA_URL_PREFIX)
+ || externalForm.endsWith(".json");
+ }
+
+ @Override
+ protected void saveAdditionalData(@NotNull HttpRequests.Request request, @NotNull File file) throws IOException {
+ URLConnection connection = request.getConnection();
+ if (saveTag(file, connection, ETAG_HEADER)) return;
+ saveTag(file, connection, LAST_MODIFIED_HEADER);
+ }
+
+ @Nullable
+ @Override
+ protected FileType adjustFileType(@Nullable FileType type, @NotNull Url url) {
+ if (type == null && url.toExternalForm().startsWith(SCHEMA_URL_PREFIX)) {
+ // json-schema.org doesn't provide a mime-type for schemas
+ return JsonFileType.INSTANCE;
+ }
+ return super.adjustFileType(type, url);
+ }
+
+ private static boolean saveTag(@NotNull File file, @NotNull URLConnection connection, @NotNull String header) throws IOException {
+ String tag = connection.getHeaderField(header);
+ if (tag != null) {
+ String path = file.getAbsolutePath();
+ if (!path.endsWith(".json")) path += ".json";
+ File tagFile = new File(path + "." + header);
+ saveToFile(tagFile, tag);
+ return true;
+ }
+ return false;
+ }
+
+ private static void saveToFile(@NotNull File tagFile, @NotNull String headerValue) throws IOException {
+ if (!tagFile.exists()) if (!tagFile.createNewFile()) return;
+ Files.write(tagFile.toPath(), ContainerUtil.createMaybeSingletonList(headerValue));
+ }
+
+ @Override
+ public boolean isUpToDate(@NotNull Url url, @NotNull VirtualFile local) {
+ long now = System.currentTimeMillis();
+ // don't update more frequently than once in 4 hours
+ if (now - myLastUpdateTime < UPDATE_DELAY) {
+ return true;
+ }
+
+ myLastUpdateTime = now;
+ String path = local.getPath();
+
+ if (now - new File(path).lastModified() < UPDATE_DELAY) {
+ return true;
+ }
+
+ if (checkUpToDate(url, path, ETAG_HEADER)) return true;
+ if (checkUpToDate(url, path, LAST_MODIFIED_HEADER)) return true;
+
+ return false;
+ }
+
+ private boolean checkUpToDate(@NotNull Url url, @NotNull String path, @NotNull String header) {
+ File file = new File(path + "." + header);
+ try {
+ return isUpToDate(url, file, header);
+ }
+ catch (IOException e) {
+ // in case of an error, don't bother with update for the next UPDATE_DELAY milliseconds
+ //noinspection ResultOfMethodCallIgnored
+ new File(path).setLastModified(System.currentTimeMillis());
+ return true;
+ }
+ }
+
+ @Override
+ protected int getDefaultConnectionTimeout() {
+ return DEFAULT_CONNECT_TIMEOUT;
+ }
+
+ private boolean isUpToDate(@NotNull Url url, @NotNull File file, @NotNull String header) throws IOException {
+ List<String> strings = file.exists() ? Files.readAllLines(file.toPath()) : ContainerUtil.emptyList();
+
+ String currentTag = strings.size() > 0 ? strings.get(0) : null;
+ if (currentTag == null) return false;
+
+ String remoteTag = connect(url, HttpRequests.head(url.toExternalForm()),
+ r -> r.getConnection().getHeaderField(header));
+
+ return currentTag.equals(remoteTag);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableCellEditor.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableCellEditor.java
new file mode 100644
index 00000000..e2fc9365
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableCellEditor.java
@@ -0,0 +1,150 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.settings.mappings;
+
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
+import com.intellij.openapi.fileChooser.ex.FileTextFieldImpl;
+import com.intellij.openapi.fileChooser.ex.LocalFsFinder;
+import com.intellij.openapi.fileChooser.impl.FileChooserFactoryImpl;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.TextBrowseFolderListener;
+import com.intellij.openapi.ui.TextFieldWithBrowseButton;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.ui.AbstractTableCellEditor;
+import com.intellij.util.ui.JBUI;
+import com.jetbrains.jsonSchema.JsonMappingKind;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.io.File;
+
+class JsonMappingsTableCellEditor extends AbstractTableCellEditor {
+
+ final TextFieldWithBrowseButton myComponent;
+ final JPanel myWrapper;
+ private final UserDefinedJsonSchemaConfiguration.Item myItem;
+ private final Project myProject;
+ private final TreeUpdater myTreeUpdater;
+
+ JsonMappingsTableCellEditor(UserDefinedJsonSchemaConfiguration.Item item, Project project, TreeUpdater treeUpdater) {
+ myItem = item;
+ myProject = project;
+ myTreeUpdater = treeUpdater;
+ myComponent = new TextFieldWithBrowseButton() {
+ @Override
+ protected void installPathCompletion(FileChooserDescriptor fileChooserDescriptor, Disposable parent) {
+ // do nothing
+ }
+ };
+ myWrapper = new JPanel();
+ myWrapper.setBorder(JBUI.Borders.empty(-3, 0));
+ myWrapper.setLayout(new BorderLayout());
+ JLabel label = new JLabel(item.mappingKind.getPrefix().trim(), item.mappingKind.getIcon(), SwingConstants.LEFT);
+ label.setBorder(JBUI.Borders.emptyLeft(1));
+ myWrapper.add(label,
+ BorderLayout.LINE_START);
+ myWrapper.add(myComponent,
+ BorderLayout.CENTER);
+ FileChooserDescriptor descriptor = createDescriptor(item);
+ if (item.isPattern()) {
+ myComponent.getButton().setVisible(false);
+ }
+ else {
+ myComponent.addBrowseFolderListener(
+ new TextBrowseFolderListener(
+ descriptor, myProject) {
+ @NotNull
+ @Override
+ protected String chosenFileToResultingText(@NotNull VirtualFile chosenFile) {
+ String relativePath = VfsUtilCore.getRelativePath(chosenFile, myProject.getBaseDir());
+ return relativePath != null ? relativePath : chosenFile.getPath();
+ }
+ });
+ }
+
+
+ FileTextFieldImpl field = null;
+ if (!item.isPattern() && !ApplicationManager.getApplication().isUnitTestMode() && !ApplicationManager.getApplication().isHeadlessEnvironment()) {
+ LocalFsFinder finder = new LocalFsFinder();
+ finder.setBaseDir(new File(myProject.getBaseDir().getPath()));
+ field = new MyFileTextFieldImpl(finder, descriptor, myComponent.getTextField(), myProject, myComponent);
+ }
+
+ // avoid closing the dialog by [Enter]
+ FileTextFieldImpl finalField = field;
+ myComponent.getTextField().addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ if (e.getKeyCode() == KeyEvent.VK_ENTER && (finalField == null || !finalField.isPopupDisplayed())) {
+ stopCellEditing();
+ }
+ }
+ });
+ }
+
+ @NotNull
+ private static FileChooserDescriptor createDescriptor(UserDefinedJsonSchemaConfiguration.Item item) {
+ return item.mappingKind == JsonMappingKind.File
+ ? FileChooserDescriptorFactory.createSingleFileDescriptor()
+ : FileChooserDescriptorFactory.createSingleFolderDescriptor();
+ }
+
+ @Override
+ public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
+ myComponent.getChildComponent().setText(myItem.getPath());
+ return myWrapper;
+ }
+
+ @Override
+ public boolean stopCellEditing() {
+ myItem.setPath(myComponent.getChildComponent().getText());
+ myTreeUpdater.updateTree(true);
+ return super.stopCellEditing();
+ }
+
+ @Override
+ public Object getCellEditorValue() {
+ return myComponent.getChildComponent().getText();
+ }
+
+ private static class MyFileTextFieldImpl extends FileTextFieldImpl {
+ private final JTextField myTextField;
+ private final Project myProject;
+
+ MyFileTextFieldImpl(LocalFsFinder finder, FileChooserDescriptor descriptor, JTextField textField, Project project, Disposable parent) {
+ super(textField, finder, new LocalFsFinder.FileChooserFilter(descriptor, true),
+ FileChooserFactoryImpl.getMacroMap(), parent);
+ myTextField = textField;
+ myProject = project;
+ myAutopopup = true;
+ }
+
+ @Nullable
+ @Override
+ public VirtualFile getSelectedFile() {
+ LookupFile lookupFile = getFile();
+ return lookupFile != null ? ((LocalFsFinder.VfsFile)lookupFile).getFile() : null;
+ }
+
+ @Override
+ protected void setTextToFile(LookupFile file) {
+ String path = file.getAbsolutePath();
+ VirtualFile ioFile = VfsUtil.findFileByIoFile(new File(path), false);
+ if (ioFile == null) {
+ myTextField.setText(path);
+ return;
+ }
+ String relativePath = VfsUtilCore.getRelativePath(ioFile, myProject.getBaseDir());
+ myTextField.setText(relativePath != null ? relativePath : path);
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableView.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableView.java
new file mode 100644
index 00000000..94697695
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableView.java
@@ -0,0 +1,52 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.settings.mappings;
+
+import com.intellij.ui.SimpleTextAttributes;
+import com.intellij.ui.table.TableView;
+import com.intellij.util.ui.StatusText;
+import com.jetbrains.jsonSchema.JsonMappingKind;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.table.TableCellEditor;
+
+class JsonMappingsTableView extends TableView<UserDefinedJsonSchemaConfiguration.Item> {
+ private final StatusText myEmptyText;
+
+ JsonMappingsTableView(JsonSchemaMappingsView.MyAddActionButtonRunnable runnable) {
+ myEmptyText = new StatusText() {
+ @Override
+ protected boolean isStatusVisible() {
+ return isEmpty();
+ }
+ };
+ myEmptyText.setText("No schema mappings defined")
+ .appendSecondaryText("Add mapping for a ", SimpleTextAttributes.REGULAR_ATTRIBUTES, null);
+
+ JsonMappingKind[] values = JsonMappingKind.values();
+ for (int i = 0; i < values.length; i++) {
+ JsonMappingKind kind = values[i];
+ myEmptyText.appendSecondaryText(kind.getDescription(), SimpleTextAttributes.LINK_ATTRIBUTES,
+ e -> runnable.doRun(kind));
+ if (i < values.length - 1) {
+ myEmptyText.appendSecondaryText(", ", SimpleTextAttributes.REGULAR_ATTRIBUTES, null);
+ }
+ }
+
+ setFocusTraversalKeysEnabled(false);
+ }
+
+ @Override
+ public void setCellEditor(TableCellEditor anEditor) {
+ super.setCellEditor(anEditor);
+ if (anEditor != null) {
+ ((JsonMappingsTableCellEditor)anEditor).myComponent.getTextField().requestFocus();
+ }
+ }
+
+ @NotNull
+ @Override
+ public StatusText getEmptyText() {
+ return myEmptyText;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaConfigurable.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaConfigurable.java
new file mode 100644
index 00000000..a264808c
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaConfigurable.java
@@ -0,0 +1,219 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.settings.mappings;
+
+import com.intellij.execution.configurations.RuntimeConfigurationWarning;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.MessageType;
+import com.intellij.openapi.ui.NamedConfigurable;
+import com.intellij.openapi.util.Comparing;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
+import com.intellij.util.Function;
+import com.intellij.util.Urls;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import com.jetbrains.jsonSchema.impl.JsonSchemaReader;
+import com.jetbrains.jsonSchema.remote.JsonFileResolver;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.io.File;
+
+/**
+ * @author Irina.Chernushina on 2/2/2016.
+ */
+public class JsonSchemaConfigurable extends NamedConfigurable<UserDefinedJsonSchemaConfiguration> {
+ private final Project myProject;
+ @NotNull private final String mySchemaFilePath;
+ @NotNull private final UserDefinedJsonSchemaConfiguration mySchema;
+ @Nullable private final TreeUpdater myTreeUpdater;
+ @NotNull private final Function<? super String, String> myNameCreator;
+ private JsonSchemaMappingsView myView;
+ private String myDisplayName;
+ private String myError;
+
+ public JsonSchemaConfigurable(Project project,
+ @NotNull String schemaFilePath, @NotNull UserDefinedJsonSchemaConfiguration schema,
+ @Nullable TreeUpdater updateTree,
+ @NotNull Function<? super String, String> nameCreator) {
+ super(true, () -> {
+ if (updateTree != null) {
+ updateTree.updateTree(true);
+ }
+ });
+ myProject = project;
+ mySchemaFilePath = schemaFilePath;
+ mySchema = schema;
+ myTreeUpdater = updateTree;
+ myNameCreator = nameCreator;
+ myDisplayName = mySchema.getName();
+ }
+
+ @NotNull
+ public UserDefinedJsonSchemaConfiguration getSchema() {
+ return mySchema;
+ }
+
+ @Override
+ public void setDisplayName(String name) {
+ myDisplayName = name;
+ }
+
+ @Override
+ public UserDefinedJsonSchemaConfiguration getEditableObject() {
+ return mySchema;
+ }
+
+ @Override
+ public String getBannerSlogan() {
+ return mySchema.getName();
+ }
+
+ @Override
+ public JComponent createOptionsPanel() {
+ if (myView == null) {
+ myView = new JsonSchemaMappingsView(myProject, myTreeUpdater, s -> {
+ if (myDisplayName.startsWith(JsonSchemaMappingsConfigurable.STUB_SCHEMA_NAME)) {
+ int lastSlash = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\'));
+ if (lastSlash > 0) {
+ String substring = s.substring(lastSlash + 1);
+ int dot = substring.lastIndexOf('.');
+ if (dot != -1) {
+ substring = substring.substring(0, dot);
+ }
+ setDisplayName(myNameCreator.fun(substring));
+ updateName();
+ }
+ }
+ });
+ myView.setError(myError, true);
+ }
+ return myView.getComponent();
+ }
+
+ @Nls
+ @Override
+ public String getDisplayName() {
+ return myDisplayName;
+ }
+
+ @Nullable
+ @Override
+ public String getHelpTopic() {
+ return JsonSchemaMappingsConfigurable.SETTINGS_JSON_SCHEMA;
+ }
+
+ @Override
+ public boolean isModified() {
+ if (myView == null) return false;
+ if (!FileUtil.toSystemDependentName(mySchema.getRelativePathToSchema()).equals(myView.getSchemaSubPath())) return true;
+ if (mySchema.getSchemaVersion() != myView.getSchemaVersion()) return true;
+ return !Comparing.equal(myView.getData(), mySchema.getPatterns());
+ }
+
+ @Override
+ public void apply() throws ConfigurationException {
+ if (myView == null) return;
+ doValidation();
+ mySchema.setName(myDisplayName);
+ mySchema.setSchemaVersion(myView.getSchemaVersion());
+ mySchema.setPatterns(myView.getData());
+ mySchema.setRelativePathToSchema(myView.getSchemaSubPath());
+ }
+
+ public static boolean isValidURL(@NotNull final String url) {
+ return JsonFileResolver.isHttpPath(url) && Urls.parse(url, false) != null;
+ }
+
+ private void doValidation() throws ConfigurationException {
+ String schemaSubPath = myView.getSchemaSubPath();
+
+ if (StringUtil.isEmptyOrSpaces(schemaSubPath)) {
+ throw new ConfigurationException((!StringUtil.isEmptyOrSpaces(myDisplayName) ? (myDisplayName + ": ") : "") + "Schema path is empty");
+ }
+
+ VirtualFile vFile;
+ String filename;
+
+ if (JsonFileResolver.isHttpPath(schemaSubPath)) {
+ filename = schemaSubPath;
+
+ if (!isValidURL(schemaSubPath)) {
+ throw new ConfigurationException(
+ (!StringUtil.isEmptyOrSpaces(myDisplayName) ? (myDisplayName + ": ") : "") + "Invalid schema URL");
+ }
+
+ vFile = JsonFileResolver.urlToFile(schemaSubPath);
+ if (vFile == null) {
+ throw new ConfigurationException(
+ (!StringUtil.isEmptyOrSpaces(myDisplayName) ? (myDisplayName + ": ") : "") + "Invalid URL resource");
+ }
+ }
+ else {
+ File subPath = new File(schemaSubPath);
+ final File file = subPath.isAbsolute() ? subPath : new File(myProject.getBasePath(), schemaSubPath);
+ if (!file.exists() || (vFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file)) == null) {
+ throw new ConfigurationException(
+ (!StringUtil.isEmptyOrSpaces(myDisplayName) ? (myDisplayName + ": ") : "") + "Schema file does not exist");
+ }
+ filename = file.getName();
+ }
+
+ if (StringUtil.isEmptyOrSpaces(myDisplayName)) throw new ConfigurationException(filename + ": Schema name is empty");
+
+ // we don't validate remote schemas while in options dialog
+ if (vFile instanceof HttpVirtualFile) return;
+
+ final String error = JsonSchemaReader.checkIfValidJsonSchema(myProject, vFile);
+ if (error != null) {
+ logErrorForUser(error);
+ throw new RuntimeConfigurationWarning(error);
+ }
+ }
+
+ private void logErrorForUser(@NotNull final String error) {
+ JsonSchemaReader.ERRORS_NOTIFICATION.createNotification(error, MessageType.WARNING).notify(myProject);
+ }
+
+ @Override
+ public void reset() {
+ if (myView == null) return;
+ myView.setItems(mySchemaFilePath, mySchema.getSchemaVersion(), mySchema.getPatterns());
+ setDisplayName(mySchema.getName());
+ }
+
+ public UserDefinedJsonSchemaConfiguration getUiSchema() {
+ final UserDefinedJsonSchemaConfiguration info = new UserDefinedJsonSchemaConfiguration();
+ info.setApplicationDefined(mySchema.isApplicationDefined());
+ if (myView != null && myView.isInitialized()) {
+ info.setName(getDisplayName());
+ info.setSchemaVersion(myView.getSchemaVersion());
+ info.setPatterns(myView.getData());
+ info.setRelativePathToSchema(myView.getSchemaSubPath());
+ } else {
+ info.setName(mySchema.getName());
+ info.setSchemaVersion(mySchema.getSchemaVersion());
+ info.setPatterns(mySchema.getPatterns());
+ info.setRelativePathToSchema(mySchema.getRelativePathToSchema());
+ }
+ return info;
+ }
+
+ @Override
+ public void disposeUIResources() {
+ if (myView != null) Disposer.dispose(myView);
+ }
+
+ public void setError(String error, boolean showWarning) {
+ myError = error;
+ if (myView != null) {
+ myView.setError(error, showWarning);
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsConfigurable.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsConfigurable.java
new file mode 100644
index 00000000..b68e2098
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsConfigurable.java
@@ -0,0 +1,333 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.settings.mappings;
+
+import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.CommonShortcuts;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.options.SearchableConfigurable;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ProjectManager;
+import com.intellij.openapi.ui.MasterDetailsComponent;
+import com.intellij.ui.EditorNotifications;
+import com.intellij.util.Function;
+import com.intellij.util.IconUtil;
+import com.intellij.util.ThreeState;
+import com.intellij.util.containers.MultiMap;
+import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.tree.DefaultTreeModel;
+import java.io.File;
+import java.util.*;
+
+import static com.jetbrains.jsonSchema.remote.JsonFileResolver.isHttpPath;
+
+/**
+ * @author Irina.Chernushina on 2/2/2016.
+ */
+public class JsonSchemaMappingsConfigurable extends MasterDetailsComponent implements SearchableConfigurable, Disposable {
+ @NonNls public static final String SETTINGS_JSON_SCHEMA = "settings.json.schema";
+ public static final String JSON_SCHEMA_MAPPINGS = "JSON Schema Mappings";
+
+ private final static Comparator<UserDefinedJsonSchemaConfiguration> COMPARATOR = (o1, o2) -> {
+ if (o1.isApplicationDefined() != o2.isApplicationDefined()) {
+ return o1.isApplicationDefined() ? 1 : -1;
+ }
+ return o1.getName().compareToIgnoreCase(o2.getName());
+ };
+ static final String STUB_SCHEMA_NAME = "New Schema";
+ private String myError;
+
+ @NotNull
+ private final Project myProject;
+ private final TreeUpdater myTreeUpdater = showWarning -> {
+ TREE_UPDATER.run();
+ updateWarningText(showWarning);
+ };
+
+ private final Function<String, String> myNameCreator = s -> createUniqueName(s);
+
+ public JsonSchemaMappingsConfigurable(@NotNull final Project project) {
+ myProject = project;
+ initTree();
+ }
+
+ @Nullable
+ @Override
+ protected String getEmptySelectionString() {
+ return myRoot.children().hasMoreElements() ? "Select JSON Schema to view" :
+ "Please add a JSON Schema file and configure its usage";
+ }
+
+ @Nullable
+ @Override
+ protected ArrayList<AnAction> createActions(boolean fromPopup) {
+ final ArrayList<AnAction> result = new ArrayList<>();
+ result.add(new DumbAwareAction("Add", "Add", IconUtil.getAddIcon()) {
+ {
+ registerCustomShortcutSet(CommonShortcuts.INSERT, myTree);
+ }
+ @Override
+ public void actionPerformed(@NotNull AnActionEvent e) {
+ addProjectSchema();
+ }
+ });
+ result.add(new MyDeleteAction());
+ return result;
+ }
+
+ public UserDefinedJsonSchemaConfiguration addProjectSchema() {
+ UserDefinedJsonSchemaConfiguration configuration = new UserDefinedJsonSchemaConfiguration(createUniqueName(STUB_SCHEMA_NAME),
+ JsonSchemaVersion.SCHEMA_4, "", false, null);
+ addCreatedMappings(configuration);
+ return configuration;
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ @NotNull
+ private String createUniqueName(@NotNull String s) {
+ int max = -1;
+ Enumeration children = myRoot.children();
+ while (children.hasMoreElements()) {
+ Object element = children.nextElement();
+ if (!(element instanceof MyNode)) continue;
+ String displayName = ((MyNode)element).getDisplayName();
+ if (displayName.startsWith(s)) {
+ String lastPart = displayName.substring(s.length()).trim();
+ if (lastPart.length() == 0 && max == -1) {
+ max = 1;
+ continue;
+ }
+ int i = tryParseInt(lastPart);
+ if (i == -1) continue;
+ max = i > max ? i : max;
+ }
+ }
+ return max == -1 ? s : (s + " " + (max + 1));
+ }
+
+ private static int tryParseInt(@NotNull String s) {
+ try {
+ return Integer.parseInt(s);
+ }
+ catch (NumberFormatException e) {
+ return -1;
+ }
+ }
+
+ private void addCreatedMappings(@NotNull final UserDefinedJsonSchemaConfiguration info) {
+ final JsonSchemaConfigurable configurable = new JsonSchemaConfigurable(myProject, "", info, myTreeUpdater, myNameCreator);
+ configurable.setError(myError, true);
+ final MyNode node = new MyNode(configurable);
+ addNode(node, myRoot);
+ selectNodeInTree(node, true);
+ }
+
+ private void fillTree() {
+ myRoot.removeAllChildren();
+
+ if (myProject.isDefault()) return;
+
+ final List<UserDefinedJsonSchemaConfiguration> list = getStoredList();
+ for (UserDefinedJsonSchemaConfiguration info : list) {
+ String pathToSchema = info.getRelativePathToSchema();
+ final JsonSchemaConfigurable configurable =
+ new JsonSchemaConfigurable(myProject, isHttpPath(pathToSchema) || new File(pathToSchema).isAbsolute() ? pathToSchema : new File(myProject.getBasePath(), pathToSchema).getPath(),
+ info, myTreeUpdater, myNameCreator);
+ configurable.setError(myError, true);
+ myRoot.add(new MyNode(configurable));
+ }
+ ((DefaultTreeModel) myTree.getModel()).reload(myRoot);
+ if (myRoot.children().hasMoreElements()) {
+ myTree.addSelectionRow(0);
+ }
+ }
+
+ @NotNull
+ private List<UserDefinedJsonSchemaConfiguration> getStoredList() {
+ final List<UserDefinedJsonSchemaConfiguration> list = new ArrayList<>();
+ final Map<String, UserDefinedJsonSchemaConfiguration> projectState = JsonSchemaMappingsProjectConfiguration
+ .getInstance(myProject).getStateMap();
+ if (projectState != null) {
+ list.addAll(projectState.values());
+ }
+
+ Collections.sort(list, COMPARATOR);
+ return list;
+ }
+
+ @Override
+ public void apply() throws ConfigurationException {
+ final List<UserDefinedJsonSchemaConfiguration> uiList = getUiList(true);
+ validate(uiList);
+ final Map<String, UserDefinedJsonSchemaConfiguration> projectMap = new HashMap<>();
+ for (UserDefinedJsonSchemaConfiguration info : uiList) {
+ projectMap.put(info.getName(), info);
+ }
+
+ JsonSchemaMappingsProjectConfiguration.getInstance(myProject).setState(projectMap);
+ final Project[] projects = ProjectManager.getInstance().getOpenProjects();
+ for (Project project : projects) {
+ final JsonSchemaService service = JsonSchemaService.Impl.get(project);
+ if (service != null) service.reset();
+ }
+ DaemonCodeAnalyzer.getInstance(myProject).restart();
+ EditorNotifications.getInstance(myProject).updateAllNotifications();
+ }
+
+ private static void validate(@NotNull List<UserDefinedJsonSchemaConfiguration> list) throws ConfigurationException {
+ final Set<String> set = new HashSet<>();
+ for (UserDefinedJsonSchemaConfiguration info : list) {
+ if (set.contains(info.getName())) {
+ throw new ConfigurationException("Duplicate schema name: '" + info.getName() + "'");
+ }
+ set.add(info.getName());
+ }
+ }
+
+ @Override
+ public boolean isModified() {
+ final List<UserDefinedJsonSchemaConfiguration> storedList = getStoredList();
+ final List<UserDefinedJsonSchemaConfiguration> uiList;
+ try {
+ uiList = getUiList(false);
+ }
+ catch (ConfigurationException e) {
+ //will not happen
+ return false;
+ }
+ return !storedList.equals(uiList);
+ }
+
+ private void updateWarningText(boolean showWarning) {
+ final MultiMap<String, UserDefinedJsonSchemaConfiguration.Item> patternsMap = new MultiMap<>();
+ final StringBuilder sb = new StringBuilder();
+ final List<UserDefinedJsonSchemaConfiguration> list;
+ try {
+ list = getUiList(false);
+ }
+ catch (ConfigurationException e) {
+ // will not happen
+ return;
+ }
+ for (UserDefinedJsonSchemaConfiguration info : list) {
+ info.refreshPatterns();
+ final JsonSchemaPatternComparator comparator = new JsonSchemaPatternComparator(myProject);
+ final List<UserDefinedJsonSchemaConfiguration.Item> patterns = info.getPatterns();
+ for (UserDefinedJsonSchemaConfiguration.Item pattern : patterns) {
+ for (Map.Entry<String, Collection<UserDefinedJsonSchemaConfiguration.Item>> entry : patternsMap.entrySet()) {
+ for (UserDefinedJsonSchemaConfiguration.Item item : entry.getValue()) {
+ final ThreeState similar = comparator.isSimilar(pattern, item);
+ if (ThreeState.NO.equals(similar)) continue;
+
+ if (sb.length() > 0) sb.append('\n');
+ sb.append("'").append(pattern.getPresentation()).append("' for schema '")
+ .append(info.getName()).append("' and '").append(item.getPresentation()).append("' for schema '").append(entry.getKey())
+ .append("'");
+ }
+ }
+ }
+ patternsMap.put(info.getName(), patterns);
+ }
+ if (sb.length() > 0) {
+ myError = "Conflicting mappings:\n" + sb.toString();
+ } else {
+ myError = null;
+ }
+ final Enumeration children = myRoot.children();
+ while (children.hasMoreElements()) {
+ Object o = children.nextElement();
+ if (o instanceof MyNode && ((MyNode)o).getConfigurable() instanceof JsonSchemaConfigurable) {
+ ((JsonSchemaConfigurable) ((MyNode)o).getConfigurable()).setError(myError, showWarning);
+ }
+ }
+ }
+
+ public void selectInTree(UserDefinedJsonSchemaConfiguration configuration) {
+ final Enumeration children = myRoot.children();
+ while (children.hasMoreElements()) {
+ final MyNode node = (MyNode)children.nextElement();
+ JsonSchemaConfigurable configurable = (JsonSchemaConfigurable)node.getConfigurable();
+ if (Objects.equals(configurable.getUiSchema(), configuration)) {
+ selectNodeInTree(node);
+ }
+ }
+ }
+
+ @NotNull
+ private List<UserDefinedJsonSchemaConfiguration> getUiList(boolean applyChildren) throws ConfigurationException {
+ final List<UserDefinedJsonSchemaConfiguration> uiList = new ArrayList<>();
+ final Enumeration children = myRoot.children();
+ while (children.hasMoreElements()) {
+ final MyNode node = (MyNode)children.nextElement();
+ if (applyChildren) {
+ node.getConfigurable().apply();
+ uiList.add(getSchemaInfo(node));
+ }
+ else {
+ uiList.add(((JsonSchemaConfigurable) node.getConfigurable()).getUiSchema());
+ }
+ }
+ Collections.sort(uiList, COMPARATOR);
+ return uiList;
+ }
+
+ @Override
+ public void reset() {
+ fillTree();
+ updateWarningText(true);
+ }
+
+ @Override
+ protected Comparator<MyNode> getNodeComparator() {
+ return (o1, o2) -> {
+ if (o1.getConfigurable() instanceof JsonSchemaConfigurable && o2.getConfigurable() instanceof JsonSchemaConfigurable) {
+ return COMPARATOR.compare(getSchemaInfo(o1), getSchemaInfo(o2));
+ }
+ return o1.getDisplayName().compareToIgnoreCase(o2.getDisplayName());
+ };
+ }
+
+ private static UserDefinedJsonSchemaConfiguration getSchemaInfo(@NotNull final MyNode node) {
+ return ((JsonSchemaConfigurable) node.getConfigurable()).getSchema();
+ }
+
+ @Nls
+ @Override
+ public String getDisplayName() {
+ return JSON_SCHEMA_MAPPINGS;
+ }
+
+
+ @Override
+ public void dispose() {
+ final Enumeration children = myRoot.children();
+ while (children.hasMoreElements()) {
+ Object o = children.nextElement();
+ if (o instanceof MyNode) {
+ ((MyNode)o).getConfigurable().disposeUIResources();
+ }
+ }
+ }
+
+ @NotNull
+ @Override
+ public String getId() {
+ return SETTINGS_JSON_SCHEMA;
+ }
+
+ @Override
+ public String getHelpTopic() {
+ return SETTINGS_JSON_SCHEMA;
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsView.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsView.java
new file mode 100644
index 00000000..795a59c0
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsView.java
@@ -0,0 +1,339 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.settings.mappings;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.ide.util.PsiNavigationSupport;
+import com.intellij.json.JsonBundle;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.actionSystem.CommonShortcuts;
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.ComboBox;
+import com.intellij.openapi.ui.MessageType;
+import com.intellij.openapi.ui.TextFieldWithBrowseButton;
+import com.intellij.openapi.ui.panel.ComponentPanelBuilder;
+import com.intellij.openapi.ui.popup.Balloon;
+import com.intellij.openapi.ui.popup.BalloonBuilder;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.ui.popup.PopupStep;
+import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.AnActionButton;
+import com.intellij.ui.AnActionButtonRunnable;
+import com.intellij.ui.DocumentAdapter;
+import com.intellij.ui.ToolbarDecorator;
+import com.intellij.ui.awt.RelativePoint;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.ui.components.JBTextField;
+import com.intellij.ui.table.TableView;
+import com.intellij.util.ui.*;
+import com.jetbrains.jsonSchema.JsonMappingKind;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import com.jetbrains.jsonSchema.extension.JsonSchemaInfo;
+import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.table.DefaultTableCellRenderer;
+import javax.swing.table.TableCellEditor;
+import javax.swing.table.TableCellRenderer;
+import java.awt.*;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import static com.jetbrains.jsonSchema.remote.JsonFileResolver.isHttpPath;
+
+/**
+ * @author Irina.Chernushina on 2/2/2016.
+ */
+public class JsonSchemaMappingsView implements Disposable {
+ private static final String ADD_SCHEMA_MAPPING = "settings.json.schema.add.mapping";
+ private static final String EDIT_SCHEMA_MAPPING = "settings.json.schema.edit.mapping";
+ private static final String REMOVE_SCHEMA_MAPPING = "settings.json.schema.remove.mapping";
+ private final TreeUpdater myTreeUpdater;
+ private final Consumer<? super String> mySchemaPathChangedCallback;
+ private TableView<UserDefinedJsonSchemaConfiguration.Item> myTableView;
+ private JComponent myComponent;
+ private Project myProject;
+ private TextFieldWithBrowseButton mySchemaField;
+ private ComboBox<JsonSchemaVersion> mySchemaVersionComboBox;
+ private JEditorPane myError;
+ private String myErrorText;
+ private JBLabel myErrorIcon;
+ private boolean myInitialized;
+
+ public JsonSchemaMappingsView(Project project,
+ TreeUpdater treeUpdater,
+ Consumer<? super String> schemaPathChangedCallback) {
+ myTreeUpdater = treeUpdater;
+ mySchemaPathChangedCallback = schemaPathChangedCallback;
+ createUI(project);
+ }
+
+ private void createUI(final Project project) {
+ myProject = project;
+ MyAddActionButtonRunnable addActionButtonRunnable = new MyAddActionButtonRunnable();
+
+ myTableView = new JsonMappingsTableView(addActionButtonRunnable);
+ myTableView.getTableHeader().setVisible(false);
+ final ToolbarDecorator decorator = ToolbarDecorator.createDecorator(myTableView);
+ final MyEditActionButtonRunnableImpl editAction = new MyEditActionButtonRunnableImpl();
+ decorator.setRemoveAction(new MyRemoveActionButtonRunnable())
+ .setRemoveActionName(REMOVE_SCHEMA_MAPPING)
+ .setAddAction(addActionButtonRunnable)
+ .setAddActionName(JsonBundle.message(ADD_SCHEMA_MAPPING))
+ .setEditAction(editAction)
+ .setEditActionName(JsonBundle.message(EDIT_SCHEMA_MAPPING))
+ .disableUpDownActions();
+
+ JBTextField schemaFieldBacking = new JBTextField();
+ mySchemaField = new TextFieldWithBrowseButton(schemaFieldBacking);
+ SwingHelper.installFileCompletionAndBrowseDialog(myProject, mySchemaField, JsonBundle.message("json.schema.add.schema.chooser.title"),
+ FileChooserDescriptorFactory.createSingleFileDescriptor());
+ mySchemaField.getTextField().getDocument().addDocumentListener(new DocumentAdapter() {
+ @Override
+ protected void textChanged(@NotNull DocumentEvent e) {
+ mySchemaPathChangedCallback.accept(mySchemaField.getText());
+ }
+ });
+ attachNavigateToSchema();
+ myError = SwingHelper.createHtmlLabel(JsonBundle.message("json.schema.conflicting.mappings"), null, s -> {
+ final BalloonBuilder builder = JBPopupFactory.getInstance().
+ createHtmlTextBalloonBuilder(myErrorText, UIUtil.getBalloonWarningIcon(), MessageType.WARNING.getPopupBackground(), null);
+ builder.setDisposable(this);
+ builder.setHideOnClickOutside(true);
+ builder.setCloseButtonEnabled(true);
+ builder.createBalloon().showInCenterOf(myError);
+ });
+
+ final FormBuilder builder = FormBuilder.createFormBuilder();
+ final JBLabel label = new JBLabel(JsonBundle.message("json.schema.file.selector.title"));
+ builder.addLabeledComponent(label, mySchemaField);
+ label.setLabelFor(mySchemaField);
+ label.setBorder(JBUI.Borders.empty(0, 10));
+ mySchemaField.setBorder(JBUI.Borders.emptyRight(10));
+ JBLabel versionLabel = new JBLabel("Schema version:");
+ mySchemaVersionComboBox = new ComboBox<>(new DefaultComboBoxModel<>(JsonSchemaVersion.values()));
+ versionLabel.setLabelFor(mySchemaVersionComboBox);
+ versionLabel.setBorder(JBUI.Borders.empty(0, 10));
+ builder.addLabeledComponent(versionLabel, mySchemaVersionComboBox);
+ final JPanel wrapper = new JPanel(new BorderLayout());
+ wrapper.setBorder(JBUI.Borders.empty(0, 10));
+ myErrorIcon = new JBLabel(UIUtil.getBalloonWarningIcon());
+ wrapper.add(myErrorIcon, BorderLayout.WEST);
+ wrapper.add(myError, BorderLayout.CENTER);
+ builder.addComponent(wrapper);
+ JPanel panel = decorator.createPanel();
+ panel.setBorder(BorderFactory.createCompoundBorder(JBUI.Borders.empty(0, 8), panel.getBorder()));
+ builder.addComponentFillVertically(panel, 5);
+ JLabel commentComponent = ComponentPanelBuilder.createCommentComponent("Path to file or directory relative to project root, or file name pattern like *.config.json", false);
+ commentComponent.setBorder(JBUI.Borders.empty(0, 8, 5, 0));
+ builder.addComponent(commentComponent);
+
+ myComponent = builder.getPanel();
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ public void setError(final String text, boolean showWarning) {
+ myErrorText = text;
+ myError.setVisible(showWarning && text != null);
+ myErrorIcon.setVisible(showWarning && text != null);
+ }
+
+ private void attachNavigateToSchema() {
+ DumbAwareAction.create(e -> {
+ String pathToSchema = mySchemaField.getText();
+ if (StringUtil.isEmptyOrSpaces(pathToSchema) || isHttpPath(pathToSchema)) return;
+ VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(new File(pathToSchema));
+ if (virtualFile == null) {
+ BalloonBuilder balloonBuilder = JBPopupFactory.getInstance()
+ .createHtmlTextBalloonBuilder(JsonBundle.message("json.schema.file.not.found"), UIUtil.getBalloonErrorIcon(), MessageType.ERROR.getPopupBackground(), null);
+ Balloon balloon = balloonBuilder.setFadeoutTime(TimeUnit.SECONDS.toMillis(3)).createBalloon();
+ balloon.showInCenterOf(mySchemaField);
+ return;
+ }
+ PsiNavigationSupport.getInstance().createNavigatable(myProject, virtualFile, -1).navigate(true);
+ }).registerCustomShortcutSet(CommonShortcuts.getEditSource(), mySchemaField);
+ }
+
+ public List<UserDefinedJsonSchemaConfiguration.Item> getData() {
+ return Collections.unmodifiableList(
+ myTableView.getListTableModel().getItems().stream()
+ .filter(i -> i.mappingKind == JsonMappingKind.Directory || !StringUtil.isEmpty(i.path))
+ .collect(Collectors.toList()));
+ }
+
+ public void setItems(String schemaFilePath,
+ JsonSchemaVersion version,
+ final List<UserDefinedJsonSchemaConfiguration.Item> data) {
+ myInitialized = true;
+ mySchemaField.setText(schemaFilePath);
+ mySchemaVersionComboBox.setSelectedItem(version);
+ myTableView.setModelAndUpdateColumns(
+ new ListTableModel<>(createColumns(), new ArrayList<>(data)));
+ }
+
+ public boolean isInitialized() {
+ return myInitialized;
+ }
+
+ public JsonSchemaVersion getSchemaVersion() {
+ return (JsonSchemaVersion)mySchemaVersionComboBox.getSelectedItem();
+ }
+
+ public String getSchemaSubPath() {
+ String schemaFieldText = mySchemaField.getText();
+ if (isHttpPath(schemaFieldText)) return schemaFieldText;
+ return FileUtil.toSystemDependentName(JsonSchemaInfo.getRelativePath(myProject, schemaFieldText));
+ }
+
+ private ColumnInfo[] createColumns() {
+ return new ColumnInfo[] { new MappingItemColumnInfo() };
+ }
+
+ public JComponent getComponent() {
+ return myComponent;
+ }
+
+ private class MappingItemColumnInfo extends ColumnInfo<UserDefinedJsonSchemaConfiguration.Item, String> {
+ MappingItemColumnInfo() {super("");}
+
+ @Nullable
+ @Override
+ public String valueOf(UserDefinedJsonSchemaConfiguration.Item item) {
+ return item.getPresentation();
+ }
+
+ @NotNull
+ @Override
+ public TableCellRenderer getRenderer(UserDefinedJsonSchemaConfiguration.Item item) {
+ return new DefaultTableCellRenderer() {
+ @Override
+ public Component getTableCellRendererComponent(JTable table,
+ Object value,
+ boolean isSelected,
+ boolean hasFocus,
+ int row,
+ int column) {
+ JLabel label = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+ label.setIcon(item.mappingKind.getIcon());
+
+ String error = item.getError();
+ if (error == null) {
+ return label;
+ }
+
+ JPanel panel = new JPanel();
+ panel.setLayout(new BorderLayout());
+ panel.add(label, BorderLayout.CENTER);
+ JLabel warning = new JLabel(AllIcons.General.Warning);
+ panel.setBackground(label.getBackground());
+ panel.setToolTipText(error);
+ panel.add(warning, BorderLayout.LINE_END);
+ return panel;
+ }
+ };
+ }
+
+ @Nullable
+ @Override
+ public TableCellEditor getEditor(UserDefinedJsonSchemaConfiguration.Item item) {
+ return new JsonMappingsTableCellEditor(item, myProject, myTreeUpdater);
+ }
+
+ @Override
+ public boolean isCellEditable(UserDefinedJsonSchemaConfiguration.Item item) {
+ return true;
+ }
+ }
+
+ class MyAddActionButtonRunnable implements AnActionButtonRunnable {
+ MyAddActionButtonRunnable() {
+ super();
+ }
+
+ @Override
+ public void run(AnActionButton button) {
+ RelativePoint point = button.getPreferredPopupPoint();
+ if (point == null) {
+ point = new RelativePoint(button.getContextComponent(), new Point(0, 0));
+ }
+ JBPopupFactory.getInstance().createListPopup(new BaseListPopupStep<JsonMappingKind>(null,
+ JsonMappingKind.values()) {
+ @NotNull
+ @Override
+ public String getTextFor(JsonMappingKind value) {
+ return "Add " + StringUtil.capitalizeWords(value.getDescription(), true);
+ }
+
+ @Override
+ public Icon getIconFor(JsonMappingKind value) {
+ return value.getIcon();
+ }
+
+ @Override
+ public PopupStep onChosen(JsonMappingKind selectedValue, boolean finalChoice) {
+ if (finalChoice) {
+ return doFinalStep(() -> doRun(selectedValue));
+ }
+ return PopupStep.FINAL_CHOICE;
+ }
+ }).show(point);
+ }
+
+ void doRun(JsonMappingKind mappingKind) {
+ UserDefinedJsonSchemaConfiguration.Item currentItem = new UserDefinedJsonSchemaConfiguration.Item("", mappingKind);
+ myTableView.getListTableModel().addRow(currentItem);
+ myTableView.editCellAt(myTableView.getListTableModel().getRowCount() - 1, 0);
+
+ myTreeUpdater.updateTree(false);
+ }
+ }
+
+ private class MyEditActionButtonRunnableImpl implements AnActionButtonRunnable {
+ MyEditActionButtonRunnableImpl() {
+ super();
+ }
+
+ @Override
+ public void run(AnActionButton button) {
+ execute();
+ }
+
+ public void execute() {
+ int selectedRow = myTableView.getSelectedRow();
+ if (selectedRow == -1) return;
+ myTableView.editCellAt(selectedRow, 0);
+ }
+ }
+
+ private class MyRemoveActionButtonRunnable implements AnActionButtonRunnable {
+ @Override
+ public void run(AnActionButton button) {
+ final int[] rows = myTableView.getSelectedRows();
+ if (rows != null && rows.length > 0) {
+ int cnt = 0;
+ for (int row : rows) {
+ myTableView.getListTableModel().removeRow(row - cnt);
+ ++cnt;
+ }
+ myTableView.getListTableModel().fireTableDataChanged();
+ myTreeUpdater.updateTree(true);
+ }
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaPatternComparator.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaPatternComparator.java
new file mode 100644
index 00000000..1538a549
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaPatternComparator.java
@@ -0,0 +1,104 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.settings.mappings;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.util.BeforeAfter;
+import com.intellij.util.ThreeState;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+
+/**
+ * @author Irina.Chernushina on 2/17/2016.
+ */
+public class JsonSchemaPatternComparator {
+ @NotNull
+ private final Project myProject;
+
+ public JsonSchemaPatternComparator(@NotNull Project project) {
+ myProject = project;
+ }
+
+ @NotNull
+ public ThreeState isSimilar(@NotNull UserDefinedJsonSchemaConfiguration.Item itemLeft,
+ @NotNull UserDefinedJsonSchemaConfiguration.Item itemRight) {
+ if (itemLeft.isPattern() != itemRight.isPattern()) return ThreeState.NO;
+ if (itemLeft.isPattern()) return comparePatterns(itemLeft, itemRight);
+ return comparePaths(itemLeft, itemRight);
+ }
+
+ private ThreeState comparePaths(UserDefinedJsonSchemaConfiguration.Item left, UserDefinedJsonSchemaConfiguration.Item right) {
+ String leftPath = left.getPath();
+ String rightPath = right.getPath();
+
+ if (leftPath.startsWith("mock:///") || rightPath.startsWith("mock:///")) {
+ return leftPath.equals(rightPath) ? ThreeState.YES : ThreeState.NO;
+ }
+ final File leftFile = new File(myProject.getBasePath(), leftPath);
+ final File rightFile = new File(myProject.getBasePath(), rightPath);
+
+ if (left.isDirectory()) {
+ if (FileUtil.isAncestor(leftFile, rightFile, true)) return ThreeState.YES;
+ }
+ if (right.isDirectory()) {
+ if (FileUtil.isAncestor(rightFile, leftFile, true)) return ThreeState.YES;
+ }
+ return FileUtil.filesEqual(leftFile, rightFile) && left.isDirectory() == right.isDirectory() ? ThreeState.YES : ThreeState.NO;
+ }
+
+ private static ThreeState comparePatterns(@NotNull final UserDefinedJsonSchemaConfiguration.Item leftItem,
+ @NotNull final UserDefinedJsonSchemaConfiguration.Item rightItem) {
+ if (leftItem.getPath().equals(rightItem.getPath())) return ThreeState.YES;
+ if (leftItem.getPath().indexOf(File.separatorChar) >= 0 || rightItem.getPath().indexOf(File.separatorChar) >= 0) {
+ // todo: better heuristic
+ return ThreeState.NO;
+ }
+ final BeforeAfter<String> left = getBeforeAfterAroundWildCards(leftItem.getPath());
+ final BeforeAfter<String> right = getBeforeAfterAroundWildCards(rightItem.getPath());
+ if (left == null || right == null) {
+ if (left == null && right == null) return leftItem.getPath().equals(rightItem.getPath()) ? ThreeState.YES : ThreeState.NO;
+ if (left == null) {
+ return checkOneSideWithoutWildcard(leftItem, right);
+ }
+ return checkOneSideWithoutWildcard(rightItem, left);
+ }
+ if (!StringUtil.isEmptyOrSpaces(left.getBefore()) && !StringUtil.isEmptyOrSpaces(right.getBefore())) {
+ if (left.getBefore().startsWith(right.getBefore()) || right.getBefore().startsWith(left.getBefore())) {
+ return ThreeState.YES;
+ }
+ // otherwise they are different
+ return ThreeState.NO;
+ }
+ if (!StringUtil.isEmptyOrSpaces(left.getAfter()) && !StringUtil.isEmptyOrSpaces(right.getAfter())) {
+ if (left.getAfter().endsWith(right.getAfter()) || right.getAfter().endsWith(left.getAfter())) {
+ return ThreeState.YES;
+ }
+ // otherwise they are different
+ return ThreeState.NO;
+ }
+ return ThreeState.UNSURE;
+ }
+
+ @NotNull
+ private static ThreeState checkOneSideWithoutWildcard(UserDefinedJsonSchemaConfiguration.Item item, BeforeAfter<String> beforeAfter) {
+ if (!StringUtil.isEmptyOrSpaces(beforeAfter.getBefore()) && item.getPath().startsWith(beforeAfter.getBefore())) {
+ return ThreeState.YES;
+ }
+ if (!StringUtil.isEmptyOrSpaces(beforeAfter.getAfter()) && item.getPath().endsWith(beforeAfter.getAfter())) {
+ return ThreeState.YES;
+ }
+ return ThreeState.UNSURE;
+ }
+
+ @Nullable
+ private static BeforeAfter<String> getBeforeAfterAroundWildCards(@NotNull final String pattern) {
+ final int firstIdx = pattern.indexOf('*');
+ final int lastIdx = pattern.lastIndexOf('*');
+ if (firstIdx < 0 || lastIdx < 0) return null;
+ return new BeforeAfter<>(pattern.substring(0, firstIdx), pattern.substring(lastIdx + 1));
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/TreeUpdater.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/TreeUpdater.java
new file mode 100644
index 00000000..364f730e
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/TreeUpdater.java
@@ -0,0 +1,6 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.settings.mappings;
+
+public interface TreeUpdater {
+ void updateTree(boolean showWarning);
+}
diff --git a/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaInfoPopupStep.java b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaInfoPopupStep.java
new file mode 100644
index 00000000..e57ccbf8
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaInfoPopupStep.java
@@ -0,0 +1,169 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.widget;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.options.ShowSettingsUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.popup.ListSeparator;
+import com.intellij.openapi.ui.popup.PopupStep;
+import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.ui.EmptyIcon;
+import com.intellij.util.ui.JBUI;
+import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import com.jetbrains.jsonSchema.extension.JsonSchemaInfo;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.settings.mappings.JsonSchemaMappingsConfigurable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import static com.jetbrains.jsonSchema.widget.JsonSchemaStatusPopup.*;
+
+class JsonSchemaInfoPopupStep extends BaseListPopupStep<JsonSchemaInfo> {
+ private final Project myProject;
+ private final VirtualFile myVirtualFile;
+ @NotNull private final JsonSchemaService myService;
+ private static final Icon EMPTY_ICON = JBUI.scale(EmptyIcon.create(AllIcons.General.Add.getIconWidth()));
+
+ JsonSchemaInfoPopupStep(@NotNull List<JsonSchemaInfo> allSchemas, @NotNull Project project, @NotNull VirtualFile virtualFile,
+ @NotNull JsonSchemaService service) {
+ super(null, allSchemas);
+ myProject = project;
+ myVirtualFile = virtualFile;
+ myService = service;
+ }
+
+ @NotNull
+ @Override
+ public String getTextFor(JsonSchemaInfo value) {
+ return value.getDescription();
+ }
+
+ @Override
+ public Icon getIconFor(JsonSchemaInfo value) {
+ if (value == ADD_MAPPING) {
+ return AllIcons.General.Add;
+ }
+
+ if (value == EDIT_MAPPINGS) {
+ return AllIcons.Actions.Edit;
+ }
+
+ if (value == LOAD_REMOTE) {
+ return AllIcons.Actions.Refresh;
+ }
+
+ return EMPTY_ICON;
+ }
+
+ @Nullable
+ @Override
+ public ListSeparator getSeparatorAbove(JsonSchemaInfo value) {
+ List<JsonSchemaInfo> values = getValues();
+ int index = values.indexOf(value);
+ if (index - 1 >= 0) {
+ JsonSchemaInfo info = values.get(index - 1);
+ if (info == EDIT_MAPPINGS || info == ADD_MAPPING) {
+ return new ListSeparator("Registered schemas");
+ }
+ if (value.getProvider() == null && info.getProvider() != null) {
+ return new ListSeparator("SchemaStore.org schemas");
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public PopupStep onChosen(JsonSchemaInfo selectedValue, boolean finalChoice) {
+ if (finalChoice) {
+ if (selectedValue == EDIT_MAPPINGS || selectedValue == ADD_MAPPING) {
+ return doFinalStep(() -> runSchemaEditorForCurrentFile());
+ }
+ else if (selectedValue == LOAD_REMOTE) {
+ return doFinalStep(() -> myService.triggerUpdateRemote());
+ }
+ else {
+ setMapping(selectedValue, myVirtualFile, myProject);
+ return doFinalStep(() -> myService.reset());
+ }
+ }
+ return PopupStep.FINAL_CHOICE;
+ }
+
+ private void runSchemaEditorForCurrentFile() {
+ JsonSchemaMappingsConfigurable configurable = new JsonSchemaMappingsConfigurable(myProject);
+ JsonSchemaMappingsProjectConfiguration mappingsConf = JsonSchemaMappingsProjectConfiguration.getInstance(myProject);
+
+ ShowSettingsUtil.getInstance().editConfigurable(myProject, configurable, () -> {
+ UserDefinedJsonSchemaConfiguration mappingForFile = mappingsConf.findMappingForFile(myVirtualFile);
+ if (mappingForFile == null) {
+ UserDefinedJsonSchemaConfiguration configuration = configurable.addProjectSchema();
+ String relativePath = VfsUtilCore.getRelativePath(myVirtualFile, myProject.getBaseDir());
+ configuration.patterns.add(new UserDefinedJsonSchemaConfiguration.Item(
+ relativePath == null ? myVirtualFile.getUrl() : relativePath, false, false));
+ mappingForFile = configuration;
+ }
+
+ configurable.selectInTree(mappingForFile);
+ });
+ }
+
+ @Override
+ public boolean isSpeedSearchEnabled() {
+ return true;
+ }
+
+ private static void setMapping(@Nullable JsonSchemaInfo selectedValue, @NotNull VirtualFile virtualFile, @NotNull Project project) {
+ JsonSchemaMappingsProjectConfiguration configuration = JsonSchemaMappingsProjectConfiguration.getInstance(project);
+
+ VirtualFile projectBaseDir = project.getBaseDir();
+
+ UserDefinedJsonSchemaConfiguration mappingForFile = configuration.findMappingForFile(virtualFile);
+ if (mappingForFile != null) {
+ for (UserDefinedJsonSchemaConfiguration.Item pattern : mappingForFile.patterns) {
+ if (Objects.equals(VfsUtil.findRelativeFile(projectBaseDir, pattern.getPathParts()), virtualFile)
+ || virtualFile.getUrl().equals(pattern.getPath())) {
+ mappingForFile.patterns.remove(pattern);
+ if (mappingForFile.patterns.size() == 0 && mappingForFile.isApplicationDefined()) {
+ configuration.removeConfiguration(mappingForFile);
+ }
+ else {
+ mappingForFile.refreshPatterns();
+ }
+ break;
+ }
+ }
+ }
+
+ if (selectedValue == null) return;
+
+ String path = VfsUtilCore.getRelativePath(virtualFile, projectBaseDir);
+ if (path == null) {
+ path = virtualFile.getUrl();
+ }
+
+ UserDefinedJsonSchemaConfiguration existing = configuration.findMappingBySchemaInfo(selectedValue);
+ UserDefinedJsonSchemaConfiguration.Item item = new UserDefinedJsonSchemaConfiguration.Item(path, false, false);
+ if (existing != null) {
+ if (!existing.patterns.contains(item)) {
+ existing.patterns.add(item);
+ existing.refreshPatterns();
+ }
+ }
+ else {
+ configuration.addConfiguration(new UserDefinedJsonSchemaConfiguration(selectedValue.getDescription(),
+ selectedValue.getSchemaVersion(),
+ selectedValue.getUrl(project),
+ true,
+ Collections.singletonList(item)));
+ }
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusPopup.java b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusPopup.java
new file mode 100644
index 00000000..06eff237
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusPopup.java
@@ -0,0 +1,82 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.widget;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.ui.popup.ListPopup;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.jsonSchema.JsonSchemaCatalogProjectConfiguration;
+import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration;
+import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration;
+import com.jetbrains.jsonSchema.extension.JsonSchemaInfo;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class JsonSchemaStatusPopup {
+ static final JsonSchemaInfo ADD_MAPPING = new JsonSchemaInfo("") {
+ @NotNull
+ @Override
+ public String getDescription() {
+ return "New Schema Mappingā€¦";
+ }
+ };
+
+ static final JsonSchemaInfo EDIT_MAPPINGS = new JsonSchemaInfo("") {
+ @NotNull
+ @Override
+ public String getDescription() {
+ return "Edit Schema Mappingsā€¦";
+ }
+ };
+
+ static final JsonSchemaInfo LOAD_REMOTE = new JsonSchemaInfo("") {
+ @NotNull
+ @Override
+ public String getDescription() {
+ return "Load SchemaStore Mappings";
+ }
+ };
+
+ static ListPopup createPopup(@NotNull JsonSchemaService service,
+ @NotNull Project project,
+ @NotNull VirtualFile virtualFile,
+ boolean showOnlyEdit) {
+ JsonSchemaInfoPopupStep step = createPopupStep(service, project, virtualFile, showOnlyEdit);
+ return JBPopupFactory.getInstance().createListPopup(step);
+ }
+
+ @NotNull
+ static JsonSchemaInfoPopupStep createPopupStep(@NotNull JsonSchemaService service,
+ @NotNull Project project,
+ @NotNull VirtualFile virtualFile,
+ boolean showOnlyEdit) {
+ List<JsonSchemaInfo> allSchemas;
+ JsonSchemaMappingsProjectConfiguration configuration = JsonSchemaMappingsProjectConfiguration.getInstance(project);
+ UserDefinedJsonSchemaConfiguration mapping = configuration.findMappingForFile(virtualFile);
+ if (!showOnlyEdit || mapping == null) {
+ List<JsonSchemaInfo> infos = service.getAllUserVisibleSchemas();
+ Comparator<JsonSchemaInfo> comparator = Comparator.comparing(JsonSchemaInfo::getDescription, String::compareTo);
+ Stream<JsonSchemaInfo> registered = infos.stream().filter(i -> i.getProvider() != null).sorted(comparator);
+ List<JsonSchemaInfo> otherList = ContainerUtil.emptyList();
+
+ if (JsonSchemaCatalogProjectConfiguration.getInstance(project).isRemoteActivityEnabled()) {
+ otherList = infos.stream().filter(i -> i.getProvider() == null).sorted(comparator).collect(Collectors.toList());
+ if (otherList.size() == 0) {
+ otherList = ContainerUtil.createMaybeSingletonList(LOAD_REMOTE);
+ }
+ }
+ allSchemas = Stream.concat(registered, otherList.stream()).collect(Collectors.toList());
+ allSchemas.add(0, mapping == null ? ADD_MAPPING : EDIT_MAPPINGS);
+ }
+ else {
+ allSchemas = ContainerUtil.createMaybeSingletonList(EDIT_MAPPINGS);
+ }
+ return new JsonSchemaInfoPopupStep(allSchemas, project, virtualFile, service);
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidget.java b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidget.java
new file mode 100644
index 00000000..5ff023e9
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidget.java
@@ -0,0 +1,310 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.widget;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.json.JsonLanguage;
+import com.intellij.lang.Language;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import com.intellij.openapi.project.DumbService;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.popup.ListPopup;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.impl.http.FileDownloadingAdapter;
+import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
+import com.intellij.openapi.vfs.impl.http.RemoteFileInfo;
+import com.intellij.openapi.wm.StatusBarWidget;
+import com.intellij.openapi.wm.impl.status.EditorBasedStatusBarPopup;
+import com.jetbrains.jsonSchema.JsonSchemaCatalogProjectConfiguration;
+import com.jetbrains.jsonSchema.extension.*;
+import com.jetbrains.jsonSchema.ide.JsonSchemaService;
+import com.jetbrains.jsonSchema.impl.JsonSchemaConflictNotificationProvider;
+import com.jetbrains.jsonSchema.impl.JsonSchemaServiceImpl;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+class JsonSchemaStatusWidget extends EditorBasedStatusBarPopup {
+ private static final String JSON_SCHEMA_BAR = "JSON: ";
+ private static final String JSON_SCHEMA_BAR_OTHER_FILES = "Schema: ";
+ private static final String JSON_SCHEMA_TOOLTIP = "JSON Schema: ";
+ private static final String JSON_SCHEMA_TOOLTIP_OTHER_FILES = "Validated by JSON Schema: ";
+ private final JsonSchemaService myService;
+ private static final String ID = "JSONSchemaSelector";
+
+ JsonSchemaStatusWidget(Project project) {
+ super(project);
+ myService = JsonSchemaService.Impl.get(project);
+ myService.registerRemoteUpdateCallback(myUpdateCallback);
+ myService.registerResetAction(myUpdateCallback);
+ }
+
+ private final Runnable myUpdateCallback = this::update;
+
+ private static class MyWidgetState extends WidgetState {
+ boolean warning = false;
+ MyWidgetState(String toolTip, String text, boolean actionEnabled) {
+ super(toolTip, text, actionEnabled);
+ }
+
+ public boolean isWarning() {
+ return warning;
+ }
+
+ public void setWarning(boolean warning) {
+ this.warning = warning;
+ this.setIcon(warning ? AllIcons.General.Warning : null);
+ }
+ }
+
+ private boolean hasAccessToSymbols() {
+ return !DumbService.getInstance(myProject).isDumb();
+ }
+
+ @NotNull
+ @Override
+ protected WidgetState getWidgetState(@Nullable VirtualFile file) {
+ if (file == null) {
+ return WidgetState.HIDDEN;
+ }
+
+ JsonSchemaEnabler[] enablers = JsonSchemaEnabler.EXTENSION_POINT_NAME.getExtensions();
+ if (Arrays.stream(enablers).noneMatch(e -> e.isEnabledForFile(file) && e.shouldShowSwitcherWidget(file))) {
+ return WidgetState.HIDDEN;
+ }
+
+ FileType fileType = file.getFileType();
+ Language language = fileType instanceof LanguageFileType ? ((LanguageFileType)fileType).getLanguage() : null;
+ boolean isJsonFile = language instanceof JsonLanguage;
+
+ if (!hasAccessToSymbols()) {
+ return WidgetState.getDumbModeState("JSON schema service", isJsonFile ? JSON_SCHEMA_BAR : JSON_SCHEMA_BAR_OTHER_FILES);
+ }
+
+ JsonWidgetSuppressor[] suppressors = JsonWidgetSuppressor.EXTENSION_POINT_NAME.getExtensions();
+ if (Arrays.stream(suppressors).anyMatch(s -> s.suppressSwitcherWidget(file, myProject))) {
+ return WidgetState.HIDDEN;
+ }
+
+ Collection<VirtualFile> schemaFiles = myService.getSchemaFilesForFile(file);
+ if (schemaFiles.size() == 0) {
+ return getNoSchemaState();
+ }
+
+ if (schemaFiles.size() != 1) {
+ List<VirtualFile> onlyUserSchemas = schemaFiles.stream().filter(s -> {
+ JsonSchemaFileProvider provider = myService.getSchemaProvider(s);
+ return provider != null && provider.getSchemaType() == SchemaType.userSchema;
+ }).collect(Collectors.toList());
+ if (onlyUserSchemas.size() > 1) {
+ MyWidgetState state = new MyWidgetState(JsonSchemaConflictNotificationProvider.createMessage(schemaFiles, myService,
+ "<br/>", "Conflicting schemas:<br/>",
+ ""),
+ schemaFiles.size() + " schemas (!)", true);
+ state.setWarning(true);
+ return state;
+ }
+ schemaFiles = onlyUserSchemas;
+ if (schemaFiles.size() == 0) {
+ return getNoSchemaState();
+ }
+ }
+
+ VirtualFile schemaFile = schemaFiles.iterator().next();
+ schemaFile = ((JsonSchemaServiceImpl)myService).replaceHttpFileWithBuiltinIfNeeded(schemaFile);
+
+ String tooltip = isJsonFile ? JSON_SCHEMA_TOOLTIP : JSON_SCHEMA_TOOLTIP_OTHER_FILES;
+ String bar = isJsonFile ? JSON_SCHEMA_BAR : JSON_SCHEMA_BAR_OTHER_FILES;
+
+ if (schemaFile instanceof HttpVirtualFile) {
+ RemoteFileInfo info = ((HttpVirtualFile)schemaFile).getFileInfo();
+ if (info == null) return getDownloadErrorState(null);
+
+ //noinspection EnumSwitchStatementWhichMissesCases
+ switch (info.getState()) {
+ case DOWNLOADING_NOT_STARTED:
+ addDownloadingUpdateListener(info);
+ return new MyWidgetState(tooltip + getSchemaFileDesc(schemaFile), bar + getPresentableNameForFile(schemaFile),
+ true);
+ case DOWNLOADING_IN_PROGRESS:
+ addDownloadingUpdateListener(info);
+ return new MyWidgetState("Download is scheduled or in progress", "Downloading JSON schema", false);
+ case ERROR_OCCURRED:
+ return getDownloadErrorState(info.getErrorMessage());
+ }
+ }
+
+ if (!isValidSchemaFile(schemaFile)) {
+ MyWidgetState state = new MyWidgetState("File is not a schema", "JSON schema error", true);
+ state.setWarning(true);
+ return state;
+ }
+
+ JsonSchemaFileProvider provider = myService.getSchemaProvider(schemaFile);
+ if (provider != null) {
+ final boolean preferRemoteSchemas = JsonSchemaCatalogProjectConfiguration.getInstance(myProject).isPreferRemoteSchemas();
+ final String remoteSource = provider.getRemoteSource();
+ String providerName = preferRemoteSchemas && remoteSource != null && !remoteSource.endsWith("!") ? remoteSource : provider.getPresentableName();
+ String shortName = StringUtil.trimEnd(StringUtil.trimEnd(providerName, ".json"), "-schema");
+ String name = preferRemoteSchemas && remoteSource != null && !remoteSource.endsWith("!") ? bar + new JsonSchemaInfo(remoteSource).getDescription()
+ : (shortName.startsWith("JSON schema") ? shortName : (bar + shortName));
+ String kind = !preferRemoteSchemas && (provider.getSchemaType() == SchemaType.embeddedSchema || provider.getSchemaType() == SchemaType.schema)
+ ? " (bundled)"
+ : "";
+ return new MyWidgetState(tooltip + providerName + kind, name, true);
+ }
+
+ return new MyWidgetState(tooltip + getSchemaFileDesc(schemaFile), bar + getPresentableNameForFile(schemaFile),
+ true);
+ }
+
+ private void addDownloadingUpdateListener(@NotNull RemoteFileInfo info) {
+ info.addDownloadingListener(new FileDownloadingAdapter() {
+ @Override
+ public void fileDownloaded(@NotNull VirtualFile localFile) {
+ update();
+ }
+
+ @Override
+ public void errorOccurred(@NotNull String errorMessage) {
+ update();
+ }
+
+ @Override
+ public void downloadingCancelled() {
+ update();
+ }
+ });
+ }
+
+ private boolean isValidSchemaFile(@Nullable VirtualFile schemaFile) {
+ return schemaFile != null && myService.isSchemaFile(schemaFile) && myService.isApplicableToFile(schemaFile);
+ }
+
+ @Nullable
+ private static String extractNpmPackageName(@Nullable String path) {
+ if (path == null) return null;
+ int idx = path.indexOf("node_modules");
+ if (idx != -1) {
+ int trimIndex = idx + "node_modules".length() + 1;
+ if (trimIndex < path.length()) {
+ path = path.substring(trimIndex);
+ idx = StringUtil.indexOfAny(path, "\\/");
+ if (idx != -1) {
+ if (path.startsWith("@")) {
+ idx = StringUtil.indexOfAny(path, "\\/", idx + 1, path.length());
+ }
+ }
+
+ if (idx != -1) {
+ return path.substring(0, idx);
+ }
+ }
+ }
+ return null;
+ }
+
+ @NotNull
+ private static String getPresentableNameForFile(@NotNull VirtualFile schemaFile) {
+ if (schemaFile instanceof HttpVirtualFile) {
+ return new JsonSchemaInfo(schemaFile.getUrl()).getDescription();
+ }
+
+ String nameWithoutExtension = schemaFile.getNameWithoutExtension();
+ if (!JsonSchemaInfo.isVeryDumbName(nameWithoutExtension)) return nameWithoutExtension;
+
+ String path = schemaFile.getPath();
+
+ String npmPackageName = extractNpmPackageName(path);
+ return npmPackageName != null ? npmPackageName : schemaFile.getName();
+ }
+
+ @NotNull
+ private static WidgetState getDownloadErrorState(@Nullable String message) {
+ MyWidgetState state = new MyWidgetState("Error downloading schema" + (message == null ? "" : (": <br/>" + message)),
+ "JSON schema error", true);
+ state.setWarning(true);
+ return state;
+ }
+
+ @NotNull
+ private static WidgetState getNoSchemaState() {
+ return new MyWidgetState("No JSON Schema defined", "No JSON schema", true);
+ }
+
+ @NotNull
+ private static String getSchemaFileDesc(@NotNull VirtualFile schemaFile) {
+ if (schemaFile instanceof HttpVirtualFile) {
+ return schemaFile.getPresentableUrl();
+ }
+
+ String npmPackageName = extractNpmPackageName(schemaFile.getPath());
+ return schemaFile.getName() + (npmPackageName == null ? "" : (" (Package: " + npmPackageName + ")"));
+ }
+
+ @Nullable
+ @Override
+ protected ListPopup createPopup(DataContext context) {
+ final VirtualFile virtualFile = CommonDataKeys.VIRTUAL_FILE.getData(context);
+ if (virtualFile == null) return null;
+
+ Project project = getProject();
+ if (project == null) return null;
+ WidgetState state = getWidgetState(virtualFile);
+ if (!(state instanceof MyWidgetState)) return null;
+ return doCreatePopup(virtualFile, project, ((MyWidgetState)state).isWarning());
+ }
+
+ @NotNull
+ private ListPopup doCreatePopup(@NotNull VirtualFile virtualFile, @NotNull Project project, boolean showOnlyEdit) {
+ return JsonSchemaStatusPopup.createPopup(myService, project, virtualFile, showOnlyEdit);
+ }
+
+ @Override
+ protected void registerCustomListeners() {
+ class Listener implements DumbService.DumbModeListener {
+ volatile boolean isDumbMode;
+
+ @Override
+ public void enteredDumbMode() {
+ isDumbMode = true;
+ update();
+ }
+
+ @Override
+ public void exitDumbMode() {
+ isDumbMode = false;
+ update();
+ }
+ }
+
+ Listener listener = new Listener();
+ myConnection.subscribe(DumbService.DUMB_MODE, listener);
+ }
+
+ @NotNull
+ @Override
+ protected StatusBarWidget createInstance(Project project) {
+ return new JsonSchemaStatusWidget(project);
+ }
+
+ @NotNull
+ @Override
+ public String ID() {
+ return ID;
+ }
+
+ @Override
+ public void dispose() {
+ myService.unregisterRemoteUpdateCallback(myUpdateCallback);
+ myService.unregisterResetAction(myUpdateCallback);
+ super.dispose();
+ }
+}
diff --git a/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidgetProvider.java b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidgetProvider.java
new file mode 100644
index 00000000..f688cbe9
--- /dev/null
+++ b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidgetProvider.java
@@ -0,0 +1,16 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.jsonSchema.widget;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.wm.StatusBarWidget;
+import com.intellij.openapi.wm.StatusBarWidgetProvider;
+import org.jetbrains.annotations.NotNull;
+
+public class JsonSchemaStatusWidgetProvider implements StatusBarWidgetProvider {
+
+ @NotNull
+ @Override
+ public StatusBarWidget getWidget(@NotNull Project project) {
+ return new JsonSchemaStatusWidget(project);
+ }
+}
diff --git a/json/src/jsonSchema/build.xml b/json/src/jsonSchema/build.xml
new file mode 100644
index 00000000..dbf3e4af
--- /dev/null
+++ b/json/src/jsonSchema/build.xml
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<project name="JSON Schemas Update" default="ALL">
+ <property name="idea.project.home" value="${basedir}/../../../../"/>
+
+ <macrodef name="get_schema">
+ <attribute name="fromUrl" />
+ <attribute name="toFile" />
+
+ <sequential>
+ <get src="@{fromUrl}"
+ dest="@{toFile}" />
+ <local name="baseUrl"/>
+ <fixcrlf file="@{toFile}"/>
+ <property name="baseUrl" value="@{fromUrl}" />
+ <script language="javascript"><![CDATA[
+ var url = new java.net.URL(project.getProperty("baseUrl"));
+ var connection = url.openConnection();
+ var etag = connection.getHeaderField("ETag");
+ project.setProperty("etag", etag);
+ ]]></script>
+ <echo message="${etag}" file="@{toFile}.ETAG" />
+ </sequential>
+ </macrodef>
+
+ <target name="prettier">
+ <get_schema fromUrl="http://json.schemastore.org/prettierrc"
+ toFile="${idea.project.home}/contrib/prettierJS/resources/prettierrc-schema.json" />
+ <!-- Add schema ID manually -->
+ <replaceregexp file="${idea.project.home}/contrib/prettierJS/resources/prettierrc-schema.json"
+ match="&quot;definitions&quot;" replace="&quot;id&quot;: &quot;http://json.schemastore.org/prettierrc&quot;,${line.separator} &quot;definitions&quot;" />
+ </target>
+
+ <target name="tslint">
+ <get_schema fromUrl="http://json.schemastore.org/tslint"
+ toFile="${idea.project.home}/contrib/tslint/resources/tslintJsonSchema/tslint-schema.json" />
+ </target>
+
+ <target name="webpack4">
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/WebpackOptions.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpack-schema4.json" />
+ </target>
+
+ <target name="webpack4plugins">
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/BannerPlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/BannerPlugin.json" />
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/DllPlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/DllPlugin.json" />
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/DllReferencePlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/DllReferencePlugin.json" />
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/HashedModuleIdsPlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/HashedModuleIdsPlugin.json" />
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/LoaderOptionsPlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/LoaderOptionsPlugin.json" />
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/SourceMapDevToolPlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/SourceMapDevToolPlugin.json" />
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/WatchIgnorePlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/WatchIgnorePlugin.json" />
+
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/debug/ProfilingPlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/debug/ProfilingPlugin.json" />
+
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/optimize/AggressiveSplittingPlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/optimize/AggressiveSplittingPlugin.json" />
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/optimize/LimitChunkCountPlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/optimize/LimitChunkCountPlugin.json" />
+ <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/optimize/MinChunkSizePlugin.json"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/optimize/MinChunkSizePlugin.json" />
+
+ </target>
+
+ <target name="nodejs">
+ <get_schema fromUrl="http://json.schemastore.org/package"
+ toFile="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json" />
+
+ <!-- add custom properties for eslint, prettier, styleling, jest, jshint, hscs -->
+ <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json"
+ match="&quot;publishConfig&quot;"
+ replace="&quot;eslintConfig&quot; : {&quot;$ref&quot;: &quot;http://json.schemastore.org/eslintrc#&quot;},${line.separator} &quot;publishConfig&quot;" />
+ <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json"
+ match="&quot;publishConfig&quot;"
+ replace="&quot;prettier&quot; : {&quot;$ref&quot;: &quot;http://json.schemastore.org/prettierrc#&quot;},${line.separator} &quot;publishConfig&quot;" />
+ <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json"
+ match="&quot;publishConfig&quot;"
+ replace="&quot;stylelint&quot; : {&quot;$ref&quot;: &quot;http://json.schemastore.org/stylelintrc#&quot;},${line.separator} &quot;publishConfig&quot;" />
+ <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json"
+ match="&quot;publishConfig&quot;"
+ replace="&quot;jest&quot; : {&quot;type&quot;: &quot;object&quot;, &quot;$ref&quot;: &quot;https://facebook.github.io/jest/docs/configuration.html!#&quot;},${line.separator} &quot;publishConfig&quot;" />
+ <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json"
+ match="&quot;publishConfig&quot;"
+ replace="&quot;jshintConfig&quot; : {&quot;$ref&quot;: &quot;http://json.schemastore.org/jshintrc#&quot;},${line.separator} &quot;publishConfig&quot;" />
+ <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json"
+ match="&quot;publishConfig&quot;"
+ replace="&quot;jscsConfig&quot; : {&quot;$ref&quot;: &quot;http://json.schemastore.org/jscsrc#&quot;},${line.separator} &quot;publishConfig&quot;" />
+ </target>
+
+ <target name="js_schemas">
+ <get_schema fromUrl="http://json.schemastore.org/babelrc"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/.babelrc-schema.json" />
+ <get_schema fromUrl="http://json.schemastore.org/stylelintrc"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/.stylelintrc-schema.json" />
+ <replaceregexp file="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/.stylelintrc-schema.json"
+ match="&quot;definitions&quot;" replace="&quot;id&quot;: &quot;http://json.schemastore.org/stylelintrc&quot;,${line.separator}&#9;&quot;definitions&quot;" />
+ <get_schema fromUrl="http://json.schemastore.org/jsconfig"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/jsconfig-schema.json" />
+ <get_schema fromUrl="http://json.schemastore.org/tsconfig"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/tsconfig-schema.json" />
+ <get_schema fromUrl="http://json.schemastore.org/tsd"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/tsd-schema.json" />
+ <get_schema fromUrl="http://json.schemastore.org/typings"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/typings-schema.json" />
+
+ <get_schema fromUrl="http://json.schemastore.org/eslintrc"
+ toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/.eslintrc-schema.json" />
+ <replaceregexp file="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/.eslintrc-schema.json"
+ match="&quot;definitions&quot;" replace="&quot;id&quot;: &quot;http://json.schemastore.org/eslintrc&quot;,${line.separator} &quot;definitions&quot;" />
+ </target>
+
+ <target name="ALL">
+ <antcall target="js_schemas" />
+ <antcall target="prettier" />
+ <antcall target="tslint" />
+ <antcall target="nodejs" />
+ <antcall target="webpack4" />
+ <antcall target="webpack4plugins" />
+ </target>
+</project> \ No newline at end of file
diff --git a/json/src/jsonSchema/schema.json b/json/src/jsonSchema/schema.json
new file mode 100644
index 00000000..048c444f
--- /dev/null
+++ b/json/src/jsonSchema/schema.json
@@ -0,0 +1,154 @@
+{
+ "id": "http://json-schema.org/draft-04/schema#",
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Core schema meta-schema",
+ "definitions": {
+ "schemaArray": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "$ref": "#" }
+ },
+ "positiveInteger": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "positiveIntegerDefault0": {
+ "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
+ },
+ "simpleTypes": {
+ "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
+ },
+ "stringArray": {
+ "type": "array",
+ "items": { "type": "string" },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ },
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "format": "uri"
+ },
+ "$schema": {
+ "type": "string",
+ "format": "uri"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "x-intellij-html-description": {
+ "type": "string",
+ "description": "Description in html format"
+ },
+ "default": {},
+ "multipleOf": {
+ "type": "number",
+ "minimum": 0,
+ "exclusiveMinimum": true
+ },
+ "maximum": {
+ "type": "number"
+ },
+ "exclusiveMaximum": {
+ "type": "boolean",
+ "default": false
+ },
+ "minimum": {
+ "type": "number"
+ },
+ "exclusiveMinimum": {
+ "type": "boolean",
+ "default": false
+ },
+ "maxLength": { "$ref": "#/definitions/positiveInteger" },
+ "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
+ "pattern": {
+ "type": "string",
+ "format": "regex"
+ },
+ "additionalItems": {
+ "anyOf": [
+ { "type": "boolean" },
+ { "$ref": "#" }
+ ],
+ "default": {}
+ },
+ "items": {
+ "anyOf": [
+ { "$ref": "#" },
+ { "$ref": "#/definitions/schemaArray" }
+ ],
+ "default": {}
+ },
+ "maxItems": { "$ref": "#/definitions/positiveInteger" },
+ "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
+ "uniqueItems": {
+ "type": "boolean",
+ "default": false
+ },
+ "maxProperties": { "$ref": "#/definitions/positiveInteger" },
+ "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
+ "required": { "$ref": "#/definitions/stringArray" },
+ "additionalProperties": {
+ "anyOf": [
+ { "type": "boolean" },
+ { "$ref": "#" }
+ ],
+ "default": {}
+ },
+ "definitions": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "default": {}
+ },
+ "properties": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "default": {}
+ },
+ "patternProperties": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "default": {}
+ },
+ "dependencies": {
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ { "$ref": "#" },
+ { "$ref": "#/definitions/stringArray" }
+ ]
+ }
+ },
+ "enum": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "type": {
+ "anyOf": [
+ { "$ref": "#/definitions/simpleTypes" },
+ {
+ "type": "array",
+ "items": { "$ref": "#/definitions/simpleTypes" },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ ]
+ },
+ "allOf": { "$ref": "#/definitions/schemaArray" },
+ "anyOf": { "$ref": "#/definitions/schemaArray" },
+ "oneOf": { "$ref": "#/definitions/schemaArray" },
+ "not": { "$ref": "#" }
+ },
+ "dependencies": {
+ "exclusiveMaximum": [ "maximum" ],
+ "exclusiveMinimum": [ "minimum" ]
+ },
+ "default": {}
+} \ No newline at end of file
diff --git a/json/src/jsonSchema/schema06.json b/json/src/jsonSchema/schema06.json
new file mode 100644
index 00000000..5877ae97
--- /dev/null
+++ b/json/src/jsonSchema/schema06.json
@@ -0,0 +1,158 @@
+{
+ "$schema": "http://json-schema.org/draft-06/schema#",
+ "$id": "http://json-schema.org/draft-06/schema#",
+ "title": "Core schema meta-schema",
+ "definitions": {
+ "schemaArray": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "$ref": "#" }
+ },
+ "nonNegativeInteger": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "nonNegativeIntegerDefault0": {
+ "allOf": [
+ { "$ref": "#/definitions/nonNegativeInteger" },
+ { "default": 0 }
+ ]
+ },
+ "simpleTypes": {
+ "enum": [
+ "array",
+ "boolean",
+ "integer",
+ "null",
+ "number",
+ "object",
+ "string"
+ ]
+ },
+ "stringArray": {
+ "type": "array",
+ "items": { "type": "string" },
+ "uniqueItems": true,
+ "default": []
+ }
+ },
+ "type": ["object", "boolean"],
+ "properties": {
+ "$id": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "$schema": {
+ "type": "string",
+ "format": "uri"
+ },
+ "$ref": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "x-intellij-html-description": {
+ "type": "string",
+ "description": "Description in html format"
+ },
+ "default": {},
+ "examples": {
+ "type": "array",
+ "items": {}
+ },
+ "multipleOf": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "maximum": {
+ "type": "number"
+ },
+ "exclusiveMaximum": {
+ "type": "number"
+ },
+ "minimum": {
+ "type": "number"
+ },
+ "exclusiveMinimum": {
+ "type": "number"
+ },
+ "maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
+ "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
+ "pattern": {
+ "type": "string",
+ "format": "regex"
+ },
+ "additionalItems": { "$ref": "#" },
+ "items": {
+ "anyOf": [
+ { "$ref": "#" },
+ { "$ref": "#/definitions/schemaArray" }
+ ],
+ "default": {}
+ },
+ "maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
+ "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
+ "uniqueItems": {
+ "type": "boolean",
+ "default": false
+ },
+ "contains": { "$ref": "#" },
+ "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
+ "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
+ "required": { "$ref": "#/definitions/stringArray" },
+ "additionalProperties": { "$ref": "#" },
+ "definitions": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "default": {}
+ },
+ "properties": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "default": {}
+ },
+ "patternProperties": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "default": {}
+ },
+ "dependencies": {
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ { "$ref": "#" },
+ { "$ref": "#/definitions/stringArray" }
+ ]
+ }
+ },
+ "propertyNames": { "$ref": "#" },
+ "const": {},
+ "enum": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "type": {
+ "anyOf": [
+ { "$ref": "#/definitions/simpleTypes" },
+ {
+ "type": "array",
+ "items": { "$ref": "#/definitions/simpleTypes" },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ ]
+ },
+ "format": { "type": "string" },
+ "allOf": { "$ref": "#/definitions/schemaArray" },
+ "anyOf": { "$ref": "#/definitions/schemaArray" },
+ "oneOf": { "$ref": "#/definitions/schemaArray" },
+ "not": { "$ref": "#" }
+ },
+ "default": {}
+}
diff --git a/json/src/jsonSchema/schema07.json b/json/src/jsonSchema/schema07.json
new file mode 100644
index 00000000..539c6069
--- /dev/null
+++ b/json/src/jsonSchema/schema07.json
@@ -0,0 +1,172 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "http://json-schema.org/draft-07/schema#",
+ "title": "Core schema meta-schema",
+ "definitions": {
+ "schemaArray": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "$ref": "#" }
+ },
+ "nonNegativeInteger": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "nonNegativeIntegerDefault0": {
+ "allOf": [
+ { "$ref": "#/definitions/nonNegativeInteger" },
+ { "default": 0 }
+ ]
+ },
+ "simpleTypes": {
+ "enum": [
+ "array",
+ "boolean",
+ "integer",
+ "null",
+ "number",
+ "object",
+ "string"
+ ]
+ },
+ "stringArray": {
+ "type": "array",
+ "items": { "type": "string" },
+ "uniqueItems": true,
+ "default": []
+ }
+ },
+ "type": ["object", "boolean"],
+ "properties": {
+ "$id": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "$schema": {
+ "type": "string",
+ "format": "uri"
+ },
+ "$ref": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "$comment": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "x-intellij-html-description": {
+ "type": "string",
+ "description": "Description in html format"
+ },
+ "default": true,
+ "readOnly": {
+ "type": "boolean",
+ "default": false
+ },
+ "examples": {
+ "type": "array",
+ "items": true
+ },
+ "multipleOf": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "maximum": {
+ "type": "number"
+ },
+ "exclusiveMaximum": {
+ "type": "number"
+ },
+ "minimum": {
+ "type": "number"
+ },
+ "exclusiveMinimum": {
+ "type": "number"
+ },
+ "maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
+ "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
+ "pattern": {
+ "type": "string",
+ "format": "regex"
+ },
+ "additionalItems": { "$ref": "#" },
+ "items": {
+ "anyOf": [
+ { "$ref": "#" },
+ { "$ref": "#/definitions/schemaArray" }
+ ],
+ "default": true
+ },
+ "maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
+ "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
+ "uniqueItems": {
+ "type": "boolean",
+ "default": false
+ },
+ "contains": { "$ref": "#" },
+ "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
+ "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
+ "required": { "$ref": "#/definitions/stringArray" },
+ "additionalProperties": { "$ref": "#" },
+ "definitions": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "default": {}
+ },
+ "properties": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "default": {}
+ },
+ "patternProperties": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#" },
+ "propertyNames": { "format": "regex" },
+ "default": {}
+ },
+ "dependencies": {
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ { "$ref": "#" },
+ { "$ref": "#/definitions/stringArray" }
+ ]
+ }
+ },
+ "propertyNames": { "$ref": "#" },
+ "const": true,
+ "enum": {
+ "type": "array",
+ "items": true,
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "type": {
+ "anyOf": [
+ { "$ref": "#/definitions/simpleTypes" },
+ {
+ "type": "array",
+ "items": { "$ref": "#/definitions/simpleTypes" },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ ]
+ },
+ "format": { "type": "string" },
+ "contentMediaType": { "type": "string" },
+ "contentEncoding": { "type": "string" },
+ "if": {"$ref": "#"},
+ "then": {"$ref": "#"},
+ "else": {"$ref": "#"},
+ "allOf": { "$ref": "#/definitions/schemaArray" },
+ "anyOf": { "$ref": "#/definitions/schemaArray" },
+ "oneOf": { "$ref": "#/definitions/schemaArray" },
+ "not": { "$ref": "#" }
+ },
+ "default": true
+}