/* * Copyright 2003-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 jetbrains.mps.workbench.index; import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.util.Key; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.indexing.FileBasedIndex; import com.intellij.util.indexing.FileBasedIndex.InputFilter; import com.intellij.util.indexing.FileContent; import com.intellij.util.indexing.ID; import com.intellij.util.indexing.SingleEntryFileBasedIndexExtension; import com.intellij.util.indexing.SingleEntryIndexer; import com.intellij.util.io.DataExternalizer; import jetbrains.mps.extapi.model.SModelData; import jetbrains.mps.extapi.persistence.ModelFactoryService; import jetbrains.mps.extapi.persistence.datasource.DataSourceFactoryFromURL; import jetbrains.mps.extapi.persistence.datasource.DataSourceFactoryRuleService; import jetbrains.mps.extapi.persistence.datasource.URLNotSupportedException; import jetbrains.mps.fileTypes.MPSFileTypeFactory; import jetbrains.mps.persistence.IndexAwareModelFactory; import jetbrains.mps.smodel.SNodePointer; import jetbrains.mps.util.ConditionalIterable; import jetbrains.mps.workbench.findusages.ConcreteFilesGlobalSearchScope; import jetbrains.mps.workbench.goTo.index.SNodeDescriptor; import jetbrains.mps.workbench.index.ModelRootsData.Entry; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.persistence.DataSource; import org.jetbrains.mps.openapi.persistence.ModelFactory; import org.jetbrains.mps.openapi.persistence.NavigationParticipant.NavigationTarget; import org.jetbrains.mps.openapi.persistence.datasource.DataSourceType; import org.jetbrains.mps.util.Condition; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; /** * Indexes .mps files, producing an object that keeps all navigable model roots. * Note, it's not a true index, rather a caching mechanism that employs indexing infrastructure (as any * SingleEntryFileBasedIndexExtension does). There's only one key to access indexed values, and it's id of the virtual file itself, * see {@link #getFileKey(VirtualFile)}. It's not an index as one needs to know file to obtain the key (look at {@link #getValues(VirtualFile)}). */ public class RootNodeNameIndex extends SingleEntryFileBasedIndexExtension<ModelRootsData> { @NonNls private static final ID<Integer, ModelRootsData> NAME = ID.create("mps.RootNodeName"); private static final Logger LOG = LogManager.getLogger(RootNodeNameIndex.class); private static final Key<SModelData> PARSED_MODEL = new Key<SModelData>("parsed-model"); public static SModelData doModelParsing(FileContent inputData) { SModelData modelData = inputData.getUserData(PARSED_MODEL); if (modelData == null) { try { URL url = constructURLFromData(inputData); if (url == null) { LOG.error("URL cannot be created from " + inputData.getFile()); return null; } DataSourceFactoryFromURL dataSourceFactory = getDataSourceFactory(url); if (dataSourceFactory == null) { return null; } DataSource dataSource = dataSourceFactory.create(url, null); DataSourceType type = dataSource.getType(); if (type == null) { return null; } ModelFactory factory = ModelFactoryService.getInstance().getDefaultModelFactory(type); if (factory == null) { return null; } if (!(factory instanceof IndexAwareModelFactory)) { return null; } modelData = ((IndexAwareModelFactory) factory).parseSingleStream(inputData.getFileName(), new ByteArrayInputStream(inputData.getContent())); if (modelData == null) { return null; } inputData.putUserData(PARSED_MODEL, modelData); } catch (URLNotSupportedException | URISyntaxException | IOException e) { LOG.error("", e); return null; } } return modelData; } @Nullable private static DataSourceFactoryFromURL getDataSourceFactory(URL url) { DataSourceFactoryRuleService service = DataSourceFactoryRuleService.getInstance(); DataSourceFactoryFromURL dataSourceFactory = service.getFactory(url); if (dataSourceFactory == null) { LOG.error("Data Source Factory is not found for " + url); } return dataSourceFactory; } @Nullable private static URL constructURLFromData(FileContent inputData) throws MalformedURLException, URISyntaxException { return VfsUtilCore.convertToURL(inputData.getFile().getUrl()); } // FIXME No idea what's this method for, why do we care about node id serialization format. Drop public static Iterable<SNode> getRootsToIterate(SModel model) { return new ConditionalIterable<SNode>(model.getRootNodes(), new MyCondition()); } /** * @return key one needs to access indexed values */ public static int getFileKey(@NotNull VirtualFile file) { // this is what SingleEntryIndexer does to associate values with a file, and what // SingleEntryFileBasedIndexExtension shall expose in its API but does not, and every client of it shall // duplicate this implementation logic when trying to access index values (Math.abs() is often overlooked) int fileId = FileBasedIndex.getFileId(file); if (fileId < 0) { System.out.printf("!!!" + file.getPath()); } return fileId; // return Math.abs(fileId); } /** * @return cached, aka 'indexed' values associated with the model file, ready for navigation */ @NotNull public static Collection<NavigationTarget> getValues(@NotNull VirtualFile modelFile) { int fileId = RootNodeNameIndex.getFileKey(modelFile); ConcreteFilesGlobalSearchScope fileScope = new ConcreteFilesGlobalSearchScope(Collections.singleton(modelFile)); List<ModelRootsData> descriptors = FileBasedIndex.getInstance().getValues(RootNodeNameIndex.NAME, fileId, fileScope); if (descriptors.isEmpty()) { return Collections.emptyList(); } ModelRootsData modelEntry = descriptors.get(0); // key is unique for the model Collection<Entry> entries = modelEntry.getEntries(); ArrayList<NavigationTarget> rv = new ArrayList<NavigationTarget>(entries.size()); for (Entry e : entries) { rv.add(new SNodeDescriptor(e.myName, e.myConcept, new SNodePointer(modelEntry.getModelReference(), e.myNode))); } return rv; } @Override @NotNull public ID<Integer, ModelRootsData> getName() { return NAME; } @NotNull @Override public DataExternalizer<ModelRootsData> getValueExternalizer() { return new ModelRootsExternalizer(); } @NotNull @Override public SingleEntryIndexer<ModelRootsData> getIndexer() { return new MyIndexer(); } @NotNull @Override public InputFilter getInputFilter() { return new MyInputFilter(); } @Override public int getVersion() { return 1; } private static class MyCondition implements Condition<SNode> { @Override public boolean met(SNode node) { // FIXME I've got no idea why we discriminate nodes with such id return !node.getNodeId().toString().contains("$"); } } private static class MyInputFilter implements FileBasedIndex.InputFilter { @Override public boolean acceptInput(@NotNull VirtualFile file) { FileType fileType = file.getFileType(); return MPSFileTypeFactory.MPS_FILE_TYPE.equals(fileType) || MPSFileTypeFactory.MPS_BINARY_FILE_TYPE.equals(fileType); } } private static class MyIndexer extends SingleEntryIndexer<ModelRootsData> { private MyIndexer() { super(false); } @Override protected ModelRootsData computeValue(@NotNull final FileContent inputData) { try { // XXX Perhaps, shall extend xml.persistence.Indexer with proper methods (name, concept) not to read as complete SModel? SModelData modelData = doModelParsing(inputData); if (modelData == null) { // e.g. model with merge conflict return null; } ModelRootsData data = new ModelRootsData(modelData); // it looks there's no reason to serialize data for empty model return data.isEmpty() ? null : data; } catch (Exception e) { LOG.error("Cannot index model file " + inputData.getFileName() + "; " + e.getMessage()); } return null; } } }