/*
* 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;
}
}
}