WEB-32238 JSON Schema: dropdown to choose JSON schemas

This commit is contained in:
Anton Lobov
2018-04-24 16:07:19 +02:00
parent 70e7721249
commit c3a3e04cb7
17 changed files with 874 additions and 55 deletions
@@ -84,9 +84,11 @@ public class JsonSchemaMappingsConfigurable extends MasterDetailsComponent imple
return result;
}
private void addProjectSchema() {
addCreatedMappings(new UserDefinedJsonSchemaConfiguration(createUniqueName(STUB_SCHEMA_NAME),
JsonSchemaVersion.SCHEMA_4, "", false, null));
public UserDefinedJsonSchemaConfiguration addProjectSchema() {
UserDefinedJsonSchemaConfiguration configuration = new UserDefinedJsonSchemaConfiguration(createUniqueName(STUB_SCHEMA_NAME),
JsonSchemaVersion.SCHEMA_4, "", false, null);
addCreatedMappings(configuration);
return configuration;
}
@SuppressWarnings("SameParameterValue")
@@ -9,22 +9,46 @@ 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.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.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
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 (Objects.equals(value.getUrl(), configuration.getRelativePathToSchema())) return configuration;
}
return null;
}
@Nullable
public UserDefinedJsonSchemaConfiguration findMappingForFile(VirtualFile file) {
VirtualFile projectBaseDir = myProject.getBaseDir();
for (UserDefinedJsonSchemaConfiguration configuration : myState.myState.values()) {
for (UserDefinedJsonSchemaConfiguration.Item pattern : configuration.patterns) {
if (pattern.pattern || pattern.directory) continue;
String path = pattern.path;
VirtualFile relativeFile = VfsUtil.findRelativeFile(projectBaseDir, path);
if (Objects.equals(relativeFile, file)) {
return configuration;
}
}
}
return null;
}
public static JsonSchemaMappingsProjectConfiguration getInstance(@NotNull final Project project) {
return ServiceManager.getService(project, JsonSchemaMappingsProjectConfiguration.class);
}
@@ -51,6 +75,23 @@ public class JsonSchemaMappingsProjectConfiguration implements PersistentStateCo
});
}
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);
}
@@ -123,6 +123,10 @@ public class UserDefinedJsonSchemaConfiguration {
myCalculatedPatterns.drop();
}
public void refreshPatterns() {
myCalculatedPatterns.drop();
}
@NotNull
public List<PairProcessor<Project, VirtualFile>> getCalculatedPatterns() {
return myCalculatedPatterns.getValue();
@@ -26,4 +26,12 @@ public interface JsonSchemaFileProvider {
default String getThirdPartyApiInformation() {
return null;
}
default boolean isUserVisible() { return true; }
@NotNull
default String getPresentableName() { return getName(); }
@Nullable
default String getRemoteSource() { return null; }
}
@@ -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.jetbrains.jsonSchema.extension;
import com.intellij.openapi.util.text.StringUtil;
import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
import org.jetbrains.annotations.NotNull;
public class JsonSchemaInfo {
private final JsonSchemaFileProvider myProvider;
private final String myUrl;
public JsonSchemaInfo(@NotNull JsonSchemaFileProvider provider) {
myProvider = provider;
myUrl = null;
}
public JsonSchemaInfo(@NotNull String url) {
myUrl = url;
myProvider = null;
}
public JsonSchemaFileProvider getProvider() {
return myProvider;
}
public String getUrl() {
return myProvider != null ? myProvider.getRemoteSource() : myUrl;
}
public String getDescription() {
if (myProvider != null) {
String providerName = myProvider.getPresentableName();
return sanitizeName(providerName);
}
assert myUrl != null;
int index = StringUtil.lastIndexOfAny(myUrl, "/\\");
if (index == -1) return sanitizeName(myUrl);
String possibleName = sanitizeName(myUrl.substring(index + 1));
if (!isVeryDumbName(possibleName)) {
return possibleName;
}
int index2 = myUrl.lastIndexOf('/', index - 1);
if (index2 != -1) {
possibleName = sanitizeName(myUrl.substring(index2 + 1, index));
}
// the only weird case
if ("http://json.schemastore.org/config".equals(myUrl)) {
return "asp.net config";
}
if (!isVeryDumbName(possibleName)) {
return possibleName;
}
if (myUrl.startsWith("https://raw.githubusercontent.com/")) {
String substring = myUrl.substring("https://raw.githubusercontent.com/".length() + 1);
int slash = substring.indexOf('/');
if (slash != -1) {
int slash2 = substring.indexOf('/', slash + 1);
if (slash2 != -1) {
return sanitizeName(substring.substring(slash + 1, slash2));
}
}
}
return possibleName;
}
private static boolean isVeryDumbName(String possibleName) {
return "schema".equals(possibleName) || "config".equals(possibleName);
}
@NotNull
private static String sanitizeName(String providerName) {
return StringUtil.trimEnd(StringUtil.trimEnd(StringUtil.trimEnd(providerName, ".json"), "-schema"), ".schema");
}
public JsonSchemaVersion getSchemaVersion() {
return myProvider != null ? myProvider.getSchemaVersion() : JsonSchemaVersion.SCHEMA_4;
}
}
@@ -89,5 +89,33 @@ public class JsonSchemaProjectSelfProviderFactory implements JsonSchemaProviderF
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();
}
}
}
@@ -8,7 +8,6 @@ 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.ide.JsonSchemaService;
import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
import com.jetbrains.jsonSchema.remote.JsonFileResolver;
import org.jetbrains.annotations.NotNull;
@@ -99,8 +98,7 @@ public class JsonSchemaUserDefinedProviderFactory implements JsonSchemaProviderF
@Override
public boolean isAvailable(@NotNull VirtualFile file) {
//noinspection SimplifiableIfStatement
if (myPatterns.isEmpty() || file.isDirectory() || !file.isValid() || getSchemaFile() == null ||
JsonSchemaService.Impl.get(myProject).isSchemaFile(file)) return false;
if (myPatterns.isEmpty() || file.isDirectory() || !file.isValid() || getSchemaFile() == null) return false;
return myPatterns.stream().anyMatch(processor -> processor.process(myProject, file));
}
@@ -121,5 +119,11 @@ public class JsonSchemaUserDefinedProviderFactory implements JsonSchemaProviderF
result = 31 * result + FileUtil.pathHashCode(myFile);
return result;
}
@Nullable
@Override
public String getRemoteSource() {
return isHttpPath(myFile) ? myFile : null;
}
}
}
@@ -8,12 +8,14 @@ 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 {
@@ -35,6 +37,13 @@ public interface JsonSchemaService {
@NotNull
Collection<VirtualFile> getSchemaFilesForFile(@NotNull VirtualFile file);
void registerRemoteUpdateCallback(Runnable callback);
void unregisterRemoteUpdateCallback(Runnable callback);
void registerResetAction(Runnable action);
void unregisterResetAction(Runnable action);
void triggerUpdateRemote();
@Nullable
JsonSchemaObject getSchemaObject(@NotNull VirtualFile file);
@@ -42,7 +51,7 @@ public interface JsonSchemaService {
JsonSchemaObject getSchemaObjectForSchemaFile(@NotNull VirtualFile schemaFile);
@Nullable
VirtualFile findSchemaFileByReference(@NotNull String reference, VirtualFile referent);
VirtualFile findSchemaFileByReference(@NotNull String reference, @Nullable VirtualFile referent);
@Nullable
JsonSchemaFileProvider getSchemaProvider(@NotNull final VirtualFile schemaFile);
@@ -51,6 +60,8 @@ public interface JsonSchemaService {
ModificationTracker getAnySchemaChangeTracker();
List<JsonSchemaInfo> getAllUserVisibleSchemas();
@NotNull
static String normalizeId(@NotNull String id) {
id = id.endsWith("#") ? id.substring(0, id.length() - 1) : id;
@@ -41,17 +41,6 @@ public class JsonCachedValues {
}
private static final Key<CachedValue<String>> SCHEMA_URL_KEY = Key.create("JsonSchemaUrlCache");
@Nullable
public static VirtualFile getSchemaFileFromSchemaProperty(@NotNull VirtualFile file, @NotNull Project project) {
String schemaUrl = getSchemaUrlFromSchemaProperty(file, project);
if (schemaUrl != null) {
VirtualFile virtualFile = JsonFileResolver.resolveSchemaByReference(file, schemaUrl);
if (virtualFile != null) return virtualFile;
}
return null;
}
@Nullable
public static String getSchemaUrlFromSchemaProperty(@NotNull VirtualFile file,
@NotNull Project project) {
@@ -76,7 +65,7 @@ public class JsonCachedValues {
if (schema == null) return null;
JsonValue value = schema.getValue();
return value instanceof JsonStringLiteral ? StringUtil.trimEnd(((JsonStringLiteral)value).getValue(), "#") : null;
return value instanceof JsonStringLiteral ? ((JsonStringLiteral)value).getValue() : null;
}
private static final Key<CachedValue<String>> SCHEMA_ID_CACHE_KEY = Key.create("JsonSchemaIdCache");
@@ -64,7 +64,8 @@ public class JsonSchemaConflictNotificationProvider extends EditorNotifications.
final Collection<VirtualFile> schemaFiles = myJsonSchemaService.getSchemaFilesForFile(file);
if (schemaFiles.size() <= 1) return null;
final String message = createMessage(schemaFiles);
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);
@@ -76,9 +77,13 @@ public class JsonSchemaConflictNotificationProvider extends EditorNotifications.
return panel;
}
public String createMessage(@NotNull final Collection<VirtualFile> schemaFiles) {
public static String createMessage(@NotNull final Collection<VirtualFile> schemaFiles,
@NotNull JsonSchemaService jsonSchemaService,
@NotNull String separator,
@NotNull String prefix,
@NotNull String suffix) {
final List<Pair<Boolean, String>> pairList = schemaFiles.stream()
.map(file -> myJsonSchemaService.getSchemaProvider(file))
.map(file -> jsonSchemaService.getSchemaProvider(file))
.filter(Objects::nonNull)
.map(provider -> Pair.create(SchemaType.userSchema.equals(provider.getSchemaType()), provider.getName()))
.collect(Collectors.toList());
@@ -95,6 +100,6 @@ public class JsonSchemaConflictNotificationProvider extends EditorNotifications.
else {
return pair.getSecond();
}
}).collect(Collectors.joining("; ", "<html>There are several JSON Schemas mapped to this file: ", "</html>"));
}).collect(Collectors.joining(separator, prefix, suffix));
}
}
@@ -13,9 +13,12 @@ import com.intellij.openapi.util.Factory;
import com.intellij.openapi.util.ModificationTracker;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.impl.http.HttpVirtualFile;
import com.intellij.util.containers.ConcurrentList;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.jsonSchema.JsonSchemaVfsListener;
import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider;
import com.jetbrains.jsonSchema.extension.JsonSchemaInfo;
import com.jetbrains.jsonSchema.extension.JsonSchemaProviderFactory;
import com.jetbrains.jsonSchema.extension.SchemaType;
import com.jetbrains.jsonSchema.ide.JsonSchemaService;
@@ -80,53 +83,143 @@ public class JsonSchemaServiceImpl implements JsonSchemaService {
public void reset() {
myState.reset();
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 = myState.getFiles().stream()
.filter(file -> reference.equals(JsonCachedValues.getSchemaId(file, myProject)))
.findFirst();
final Optional<VirtualFile> optional = findBuiltInSchemaByReference(reference);
return optional.orElseGet(() -> JsonFileResolver.resolveSchemaByReference(referent, JsonSchemaService.normalizeId(reference)));
}
private Optional<VirtualFile> findBuiltInSchemaByReference(@NotNull String reference) {
return myState.getFiles().stream()
.filter(file -> JsonSchemaService.normalizeId(reference).equals(JsonCachedValues.getSchemaId(file, myProject)))
.findFirst();
}
@Override
@NotNull
public Collection<VirtualFile> getSchemaFilesForFile(@NotNull final VirtualFile file) {
List<VirtualFile> files = myState.getProviders().stream().filter(provider -> isProviderAvailable(file, provider))
.map(processor -> processor.getSchemaFile()).collect(Collectors.toList());
if (files.size() > 0) {
return files;
return getSchemasForFile(file, false);
}
@NotNull
private Collection<VirtualFile> getSchemasForFile(@NotNull VirtualFile file, boolean single) {
List<JsonSchemaFileProvider> providers = getProvidersForFile(file);
// proper priority:
// 1) user providers
// 2) $schema property
// 3) built-in providers
// 4) schema catalog
boolean checkSchemaProperty = true;
if (providers.stream().noneMatch(p -> p.getSchemaType() == SchemaType.userSchema)) {
VirtualFile virtualFile = resolveFromSchemaProperty(file);
if (virtualFile != null) return ContainerUtil.createMaybeSingletonList(virtualFile);
checkSchemaProperty = false;
}
if (!single) {
List<VirtualFile> files = ContainerUtil.newArrayList();
for (JsonSchemaFileProvider provider : providers) {
VirtualFile schemaFile = provider.getSchemaFile();
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 = selected.getSchemaFile();
return ContainerUtil.createMaybeSingletonList(schemaFile);
}
if (checkSchemaProperty) {
VirtualFile virtualFile = resolveFromSchemaProperty(file);
if (virtualFile != null) return ContainerUtil.createMaybeSingletonList(virtualFile);
}
return ContainerUtil.createMaybeSingletonList(resolveSchemaFromOtherSources(file));
}
@NotNull
private List<JsonSchemaFileProvider> getProvidersForFile(@NotNull VirtualFile file) {
return myState.getProviders().stream().filter(provider -> isProviderAvailable(file, provider)).collect(
Collectors.toList());
}
@Nullable
private VirtualFile resolveFromSchemaProperty(@NotNull VirtualFile file) {
String schemaUrl = JsonCachedValues.getSchemaUrlFromSchemaProperty(file, myProject);
if (schemaUrl != null) {
VirtualFile virtualFile = findSchemaFileByReference(schemaUrl, file);
if (virtualFile != null) return virtualFile;
}
return null;
}
public List<JsonSchemaInfo> getAllUserVisibleSchemas() {
List<String> schemas = myCatalogManager.getAllCatalogSchemas();
Collection<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()
&& provider.getRemoteSource() != null /*currently we're unable to handle providers without URLs properly*/
&& processedRemotes.add(provider.getRemoteSource())) {
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) {
List<JsonSchemaFileProvider> providers =
myState.getProviders().stream().filter(provider -> isProviderAvailable(file, provider)).collect(Collectors.toList());
Collection<VirtualFile> schemas = getSchemasForFile(file, true);
if (schemas.size() == 0) return null;
assert schemas.size() == 1;
VirtualFile schemaFile = schemas.iterator().next();
return JsonCachedValues.getSchemaObject(replaceHttpFileWithBuiltinIfNeeded(schemaFile), myProject);
}
if (providers.isEmpty()) {
VirtualFile virtualFile = resolveSchemaFromOtherSources(file);
return virtualFile == null ? null : JsonCachedValues.getSchemaObject(virtualFile, myProject);
private 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 (schemaFile instanceof HttpVirtualFile) {
String url = schemaFile.getUrl();
Optional<VirtualFile> first = myState.getFiles().stream()
.filter(f -> {
JsonSchemaFileProvider prov = getSchemaProvider(f);
return prov != null && !(prov.getSchemaFile() instanceof HttpVirtualFile) && url.equals(prov.getRemoteSource());
}).findFirst();
return first.orElse(schemaFile);
}
if (providers.size() > 2) return null;
final JsonSchemaFileProvider selected;
if (providers.size() > 1) {
final Optional<JsonSchemaFileProvider> userSchema =
providers.stream().filter(provider -> SchemaType.userSchema.equals(provider.getSchemaType())).findFirst();
if (!userSchema.isPresent()) return null;
selected = userSchema.get();
} else selected = providers.get(0);
if (selected.getSchemaFile() == null) return null;
return JsonCachedValues.getSchemaObject(selected.getSchemaFile(), myProject);
return schemaFile;
}
@Nullable
@@ -180,11 +273,36 @@ public class JsonSchemaServiceImpl implements JsonSchemaService {
@Nullable
private VirtualFile resolveSchemaFromOtherSources(@NotNull VirtualFile file) {
VirtualFile virtualFile = JsonCachedValues.getSchemaFileFromSchemaProperty(file, myProject);
if (virtualFile != null) return virtualFile;
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 triggerUpdateRemote() {
myCatalogManager.triggerUpdateCatalog();
}
private static class MyState {
@NotNull private final Factory<List<JsonSchemaFileProvider>> myFactory;
@NotNull private final AtomicClearableLazyValue<Map<VirtualFile, JsonSchemaFileProvider>> myData;
@@ -5,7 +5,9 @@ import com.intellij.openapi.application.ApplicationManager;
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.impl.JsonCachedValues;
@@ -14,6 +16,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -65,6 +68,52 @@ public class JsonSchemaCatalogManager {
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(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() {
JsonFileResolver.startFetchingHttpFileIfNeeded(myCatalog);
}
@Nullable
private static String resolveSchemaFile(@NotNull VirtualFile file, @NotNull VirtualFile catalogFile, @NotNull Project project) {
JsonFileResolver.startFetchingHttpFileIfNeeded(catalogFile);
@@ -0,0 +1,155 @@
// 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.JsonSchemaMappingsConfigurable;
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 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()));
public 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_SCHEMA) {
return AllIcons.General.Add;
}
if (value == EDIT_MAPPINGS) {
return AllIcons.General.EditItemInSection;
}
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) {
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 == ADD_SCHEMA) {
return doFinalStep(() -> {
JsonSchemaMappingsConfigurable configurable = new JsonSchemaMappingsConfigurable(myProject);
ShowSettingsUtil.getInstance().editConfigurable(myProject, configurable, () -> {
UserDefinedJsonSchemaConfiguration configuration = configurable.addProjectSchema();
configuration.patterns.add(new UserDefinedJsonSchemaConfiguration.Item(
VfsUtilCore.getRelativePath(myVirtualFile, myProject.getBaseDir()), false, false));
});
});
}
else if (selectedValue == EDIT_MAPPINGS) {
return doFinalStep(() -> ShowSettingsUtil.getInstance().editConfigurable(myProject, new JsonSchemaMappingsConfigurable(myProject)));
}
else if (selectedValue == LOAD_REMOTE) {
return doFinalStep(() -> myService.triggerUpdateRemote());
}
else {
setMapping(selectedValue, myVirtualFile, myProject);
return doFinalStep(() -> myService.reset());
}
}
return PopupStep.FINAL_CHOICE;
}
@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.path), virtualFile)) {
mappingForFile.patterns.remove(pattern);
if (mappingForFile.patterns.size() == 0) {
configuration.removeConfiguration(mappingForFile);
}
else {
mappingForFile.refreshPatterns();
}
break;
}
}
}
if (selectedValue == null) return;
String path = VfsUtilCore.getRelativePath(virtualFile, projectBaseDir);
UserDefinedJsonSchemaConfiguration existing = configuration.findMappingBySchemaInfo(selectedValue);
UserDefinedJsonSchemaConfiguration.Item item = new UserDefinedJsonSchemaConfiguration.Item(path, false, false);
if (existing != null) {
existing.patterns.add(item);
existing.refreshPatterns();
}
else {
configuration.addConfiguration(new UserDefinedJsonSchemaConfiguration(selectedValue.getDescription(),
selectedValue.getSchemaVersion(),
selectedValue.getUrl(),
false,
Collections.singletonList(item)));
}
}
}
@@ -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.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.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_SCHEMA = new JsonSchemaInfo("") {
@Override
public String getDescription() {
return "Add new schema";
}
};
static final JsonSchemaInfo EDIT_MAPPINGS = new JsonSchemaInfo("") {
@Override
public String getDescription() {
return "Edit schema mappings";
}
};
static final JsonSchemaInfo LOAD_REMOTE = new JsonSchemaInfo("") {
@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;
if (!showOnlyEdit) {
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 = 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, ADD_SCHEMA);
allSchemas.add(1, EDIT_MAPPINGS);
}
else {
allSchemas = ContainerUtil.createMaybeSingletonList(EDIT_MAPPINGS);
}
return new JsonSchemaInfoPopupStep(allSchemas, project, virtualFile, service);
}
}
@@ -0,0 +1,226 @@
// 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.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.Project;
import com.intellij.openapi.ui.popup.ListPopup;
import com.intellij.openapi.util.SystemInfo;
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.StatusBar;
import com.intellij.openapi.wm.StatusBarWidget;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.openapi.wm.impl.status.EditorBasedStatusBarPopup;
import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider;
import com.jetbrains.jsonSchema.extension.SchemaType;
import com.jetbrains.jsonSchema.ide.JsonSchemaService;
import com.jetbrains.jsonSchema.impl.JsonSchemaConflictNotificationProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
public class JsonSchemaStatusWidget {
private final Project myProject;
private final JsonSchemaService myService;
@Nullable
private MyWidget myWidget;
private final Runnable myUpdateCallback = () -> {
if (myWidget != null) {
myWidget.update();
}
};
public JsonSchemaStatusWidget(final Project project) {
myProject = project;
myService = JsonSchemaService.Impl.get(project);
myService.registerRemoteUpdateCallback(myUpdateCallback);
myService.registerResetAction(myUpdateCallback);
showOrHideWidget(false);
}
public void destroy() {
myService.unregisterRemoteUpdateCallback(myUpdateCallback);
myService.unregisterResetAction(myUpdateCallback);
showOrHideWidget(true);
}
private void showOrHideWidget(boolean forceRemove) {
StatusBar statusBar = WindowManager.getInstance().getStatusBar(myProject);
if (statusBar == null) {
return;
}
boolean showWidget = !forceRemove;
if (showWidget) {
if (myWidget == null) {
myWidget = new MyWidget(myProject);
statusBar.addWidget(myWidget, myWidget.getAnchor());
}
}
else {
if (myWidget != null) {
statusBar.removeWidget(myWidget.ID());
myWidget = null;
}
}
}
private class MyWidget extends EditorBasedStatusBarPopup {
public MyWidget(Project project) {
super(project);
}
private class MyWidgetState extends WidgetState {
boolean warning = false;
public 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);
}
}
@NotNull
@Override
protected WidgetState getWidgetState(@Nullable VirtualFile file) {
if (file == null) {
return new MyWidgetState("", "", false);
}
FileType fileType = file.getFileType();
if (!(fileType instanceof LanguageFileType) || !(((LanguageFileType)fileType).getLanguage() instanceof JsonLanguage)) {
WidgetState state = new MyWidgetState("", "", true);
state.setHidden(true);
return state;
}
Collection<VirtualFile> schemaFiles = myService.getSchemaFilesForFile(file);
if (schemaFiles.size() == 0) {
return new MyWidgetState("No JSON Schema defined", "No JSON schema", true);
}
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 new MyWidgetState("No JSON Schema defined", "No JSON schema", true);
}
}
VirtualFile schemaFile = schemaFiles.iterator().next();
JsonSchemaFileProvider provider = myService.getSchemaProvider(schemaFile);
if (provider != null) {
String providerName = provider.getPresentableName();
String shortName = StringUtil.trimEnd(StringUtil.trimEnd(providerName, ".json"), "-schema");
String name = shortName.startsWith("JSON Schema") ? shortName : ("JSON Schema: " + shortName);
String kind = provider.getSchemaType() == SchemaType.embeddedSchema || provider.getSchemaType() == SchemaType.schema ? " (bundled)" : "";
return new MyWidgetState("JSON Schema: " + providerName + kind, name, true);
}
if (schemaFile instanceof HttpVirtualFile) {
RemoteFileInfo info = ((HttpVirtualFile)schemaFile).getFileInfo();
if (info == null) {
MyWidgetState state = new MyWidgetState("Error downloading schema", "JSON schema error", true);
state.setWarning(true);
return state;
}
//noinspection EnumSwitchStatementWhichMissesCases
switch (info.getState()) {
case DOWNLOADING_NOT_STARTED:
case DOWNLOADING_IN_PROGRESS:
info.addDownloadingListener(new FileDownloadingAdapter() {
@Override
public void fileDownloaded(VirtualFile localFile) {
if (myWidget != null) {
myWidget.update();
}
}
});
return new MyWidgetState("Download is scheduled or in progress", "Downloading JSON schema", false);
case ERROR_OCCURRED:
MyWidgetState state = new MyWidgetState("Error downloading schema", "JSON schema error", true);
state.setWarning(true);
return state;
}
}
if (!myService.isSchemaFile(schemaFile)) {
MyWidgetState state = new MyWidgetState("File is not a schema", "JSON schema error", true);
state.setWarning(true);
return state;
}
return new MyWidgetState("JSON Schema: " + getSchemaFileDesc(schemaFile), "JSON Schema: " + schemaFile.getNameWithoutExtension(), true);
}
@NotNull
private String getSchemaFileDesc(VirtualFile schemaFile) {
if (schemaFile instanceof HttpVirtualFile) {
return schemaFile.getPresentableUrl();
}
return schemaFile.getName();
}
@Nullable
@Override
protected ListPopup createPopup(DataContext context) {
final VirtualFile virtualFile = CommonDataKeys.VIRTUAL_FILE.getData(context);
if (virtualFile == null) return null;
Project project = getProject();
return doCreatePopup(virtualFile, project, ((MyWidgetState)getWidgetState(virtualFile)).isWarning());
}
@NotNull
private ListPopup doCreatePopup(VirtualFile virtualFile, Project project, boolean showOnlyEdit) {
return JsonSchemaStatusPopup.createPopup(myService, project, virtualFile, showOnlyEdit);
}
@Override
protected void registerCustomListeners() {
}
@NotNull
@Override
protected StatusBarWidget createInstance(Project project) {
return new MyWidget(project);
}
@NotNull
@Override
public String ID() {
return "JSONSchemaSelector";
}
public String getAnchor() {
return "after " + (SystemInfo.isMac ? "Encoding" : "InsertOverwrite");
}
}
}
@@ -0,0 +1,23 @@
// 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.components.AbstractProjectComponent;
import com.intellij.openapi.project.Project;
public class JsonWidgetProvider extends AbstractProjectComponent {
private JsonSchemaStatusWidget myWidget;
protected JsonWidgetProvider(Project project) {
super(project);
}
@Override
public void projectOpened() {
myWidget = new JsonSchemaStatusWidget(myProject);
}
@Override
public void projectClosed() {
myWidget.destroy();
}
}
@@ -7,7 +7,9 @@
</application-components>
<project-components>
<!-- Add your project components here -->
<component>
<implementation-class>com.jetbrains.jsonSchema.widget.JsonWidgetProvider</implementation-class>
</component>
</project-components>
<actions>