/*
* Copyright 2003-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 jetbrains.mps.persistence;
import jetbrains.mps.extapi.model.SModelBase;
import jetbrains.mps.extapi.model.SModelData;
import jetbrains.mps.extapi.persistence.datasource.PreinstalledDataSourceTypes;
import jetbrains.mps.persistence.MetaModelInfoProvider.MetaInfoLoadingOption;
import jetbrains.mps.persistence.MetaModelInfoProvider.RegularMetaModelInfo;
import jetbrains.mps.persistence.MetaModelInfoProvider.StuffedMetaModelInfo;
import jetbrains.mps.project.MPSExtentions;
import jetbrains.mps.smodel.DefaultSModel;
import jetbrains.mps.smodel.DefaultSModelDescriptor;
import jetbrains.mps.smodel.SModelHeader;
import jetbrains.mps.smodel.SModelId;
import jetbrains.mps.smodel.loading.ModelLoadResult;
import jetbrains.mps.smodel.loading.ModelLoadingState;
import jetbrains.mps.smodel.persistence.def.ModelPersistence;
import jetbrains.mps.smodel.persistence.def.ModelReadException;
import jetbrains.mps.util.FileUtil;
import jetbrains.mps.util.annotation.ToRemove;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.annotations.Internal;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelName;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.persistence.DataSource;
import org.jetbrains.mps.openapi.persistence.ModelCreationException;
import org.jetbrains.mps.openapi.persistence.ModelFactory;
import org.jetbrains.mps.openapi.persistence.ModelFactoryType;
import org.jetbrains.mps.openapi.persistence.ModelLoadException;
import org.jetbrains.mps.openapi.persistence.ModelLoadingOption;
import org.jetbrains.mps.openapi.persistence.MultiStreamDataSource;
import org.jetbrains.mps.openapi.persistence.PersistenceFacade;
import org.jetbrains.mps.openapi.persistence.StreamDataSource;
import org.jetbrains.mps.openapi.persistence.UnsupportedDataSourceException;
import org.jetbrains.mps.openapi.persistence.datasource.DataSourceType;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Factory for models stored in .mps files.
*/
public class DefaultModelPersistence implements ModelFactory, IndexAwareModelFactory {
private static final Logger LOG = LogManager.getLogger(DefaultModelPersistence.class);
/**
* Boolean option for model loading, indicates loaded model doesn't care about implementation node.
* For the time being, implementation node is the one with appropriate ConceptKind (designated according to concept's implemented interfaces).
*
* @deprecated use {@link ContentLoadingExtentOptions} instead
*/
@ToRemove(version = 3.7)
@Deprecated
public static final String OPTION_STRIP_IMPLEMENTATION = "load-without-impl";
/**
* Boolean option for model loading, indicates loaded model cares about its interface aspects only.
*
* @deprecated use {@link ContentLoadingExtentOptions} instead
*/
@ToRemove(version = 3.7)
@Deprecated
public static final String OPTION_INTERFACE_ONLY = "load-interface-only";
public enum ContentLoadingExtentOptions implements ModelLoadingOption {
STRIP_IMPLEMENTATION,
INTERFACE_ONLY
}
@Internal
public DefaultModelPersistence() {
// do not delete, it is a java service
}
@NotNull
@Override
public SModel load(@NotNull DataSource dataSource, @NotNull Map<String, String> options) throws IOException {
List<ModelLoadingOption> newOptions = new ArrayList<>();
if (Boolean.parseBoolean(options.get(MetaModelInfoProvider.OPTION_KEEP_READ_METAINFO))) {
newOptions.add(MetaInfoLoadingOption.KEEP_READ);
}
if (options.containsKey(OPTION_STRIP_IMPLEMENTATION) && Boolean.parseBoolean(options.get(OPTION_STRIP_IMPLEMENTATION))) {
newOptions.add(ContentLoadingExtentOptions.STRIP_IMPLEMENTATION);
} else if (options.containsKey(OPTION_INTERFACE_ONLY) && Boolean.parseBoolean(options.get(OPTION_INTERFACE_ONLY))) {
newOptions.add(ContentLoadingExtentOptions.INTERFACE_ONLY);
}
try {
return load(dataSource, newOptions.toArray(new ModelLoadingOption[newOptions.size()]));
} catch (ModelLoadException e) {
throw new IOException(e);
}
}
@NotNull
@Override
public SModel create(@NotNull DataSource dataSource, @NotNull Map<String, String> options) throws IOException {
String modelName = options.get(OPTION_MODELNAME);
if (modelName == null) {
throw new IOException("modelName is not provided");
}
try {
return create(dataSource, new SModelName(modelName));
} catch (ModelCreationException e) {
throw new IOException(e);
}
}
@Override
public boolean canCreate(@NotNull DataSource dataSource, @NotNull Map<String, String> options) {
return dataSource instanceof StreamDataSource;
}
@Override
public boolean supports(@NotNull DataSource dataSource) {
return dataSource instanceof StreamDataSource;
}
@NotNull
@Override
public SModel create(@NotNull DataSource dataSource,
@NotNull SModelName modelName,
@NotNull ModelLoadingOption... options) throws UnsupportedDataSourceException,
ModelCreationException {
if (!(supports(dataSource))) {
throw new UnsupportedDataSourceException(dataSource);
}
final SModelHeader header = SModelHeader.create(ModelPersistence.LAST_VERSION);
final SModelReference modelReference = PersistenceFacade.getInstance().createModelReference(null, SModelId.generate(), modelName.getValue());
header.setModelReference(modelReference);
final DefaultSModelDescriptor rv = new DefaultSModelDescriptor(new PersistenceFacility(this, (StreamDataSource) dataSource), header);
// Hack to ensure newly created model is indeed empty. Otherwise, with StreamDataSource pointing to existing model stream, an attempt to
// do anything with the model triggers loading and the model get all the data. Two approaches deemed reasonable to tackle the issue:
// (a) enforce clear empty model (why would anyone call #create() then)
// (b) fail with error (too brutal?)
// Another alternative considered is to tolerate any DataSource in DefaultSModelDescriptor (or its persistence counterpart), so that
// one can create an empty model with NullDataSource, and later save with a proper DataSource (which yields more job to client and makes him
// question why SModel.save() is there). This task is reasonable regardless of final approach taken, but would take more effort, hence the hack.
if (dataSource.getTimestamp() != -1) { // chances are there's something in the stream already
rv.replace(new DefaultSModel(modelReference, header)); // model state is FULLY_LOADED, DataSource won't get read
}
return rv;
}
@NotNull
@Override
public SModel load(@NotNull DataSource dataSource, @NotNull ModelLoadingOption... options) throws UnsupportedDataSourceException,
ModelLoadException {
if (!(dataSource instanceof StreamDataSource)) {
throw new UnsupportedDataSourceException(dataSource);
}
StreamDataSource source = (StreamDataSource) dataSource;
PersistenceFacility persistenceFacility = new PersistenceFacility(this, source);
SModelHeader header = readHeader(dataSource, source, persistenceFacility);
LOG.debug("Getting model " + header.getModelReference() + " from " + dataSource.getLocation());
if (Arrays.asList(options).contains(MetaInfoLoadingOption.KEEP_READ)) {
header.setMetaInfoProvider(new StuffedMetaModelInfo(new RegularMetaModelInfo(header.getModelReference())));
}
// If there are any load options, process them and fill the model with desired model data, otherwise return a lightweight descriptor.
final DefaultSModelDescriptor resultingModel = new DefaultSModelDescriptor(persistenceFacility, header);
ModelLoadingState loadingLevel = detectLoadingLevel(options);
readModelUpToLevel(dataSource, persistenceFacility, header, resultingModel, loadingLevel);
return resultingModel;
}
private void readModelUpToLevel(@NotNull DataSource dataSource,
PersistenceFacility persistenceFacility,
SModelHeader header,
DefaultSModelDescriptor rv,
ModelLoadingState loadingLevel) throws ModelLoadException {
if (loadingLevel != null) {
try {
jetbrains.mps.smodel.SModel md = persistenceFacility.readModel(header, loadingLevel).getModel();
rv.replace(md);
} catch (ModelReadException e) {
LOG.error("Can't read model: ", e);
throw new ModelLoadException("Can't read a model from the '" + dataSource + "'", Collections.emptyList(), e);
}
}
}
@NotNull
private SModelHeader readHeader(@NotNull DataSource dataSource, StreamDataSource source, PersistenceFacility pf) throws ModelLoadException {
SModelHeader header;
try {
header = pf.readHeader();
} catch (ModelReadException e) {
LOG.error("Can't read model: ", e);
throw new ModelLoadException("Can't read model header from the '" + dataSource + "'", Collections.emptyList(), e);
}
if (header.getModelReference() == null) {
throw new ModelLoadException("Could not find model reference in the model header while loading from the " + source);
}
return header;
}
/**
* An alternative to replace() method call (which is hacky) is to expose UpdateableModel field from LazyEditableSModelBase and use
* UpdateableModel#getModel(ModelLoadingState) instead to ensure model is loaded to desired state.
* However, not sure subsequent access to model won't trigger full load anyway, thus replace() which indicates supplied state is 'FULLY LOADED'
* might be the right (hacky, nonetheless) solution.
* [atikhomirov]
*/
@Nullable
private ModelLoadingState detectLoadingLevel(@NotNull ModelLoadingOption[] options) {
ModelLoadingState loadingLevel = null;
if (Arrays.asList(options).contains(ContentLoadingExtentOptions.STRIP_IMPLEMENTATION)) {
loadingLevel = ModelLoadingState.NO_IMPLEMENTATION;
} else if (Arrays.asList(options).contains(ContentLoadingExtentOptions.INTERFACE_ONLY)) {
loadingLevel = ModelLoadingState.INTERFACE_LOADED;
}
return loadingLevel;
}
@Override
public boolean needsUpgrade(@NotNull DataSource dataSource) throws IOException {
if (!(dataSource instanceof StreamDataSource)) {
throw new UnsupportedDataSourceException(dataSource);
}
try {
SModelHeader header = ModelPersistence.loadDescriptor((StreamDataSource) dataSource);
return header.getPersistenceVersion() < ModelPersistence.LAST_VERSION;
} catch (ModelReadException ex) {
throw new IOException(ex);
}
}
@Override
public void upgrade(@NotNull DataSource dataSource) throws IOException {
if (!(dataSource instanceof StreamDataSource)) {
throw new UnsupportedDataSourceException(dataSource);
}
StreamDataSource source = (StreamDataSource) dataSource;
try {
DefaultSModel model = ModelPersistence.readModel(source, false);
ModelPersistence.saveModel(model, source, ModelPersistence.LAST_VERSION);
} catch (ModelReadException ex) {
throw new IOException(ex.getMessage(), ex);
}
}
@Override
public void save(@NotNull SModel model, @NotNull DataSource dataSource) throws IOException {
if (!(dataSource instanceof StreamDataSource)) {
throw new UnsupportedDataSourceException(dataSource);
}
int persistenceVersion = -1;
if (model instanceof PersistenceVersionAware) {
persistenceVersion = ((PersistenceVersionAware) model).getPersistenceVersion();
}
if (persistenceVersion == -1) {
persistenceVersion = ModelPersistence.LAST_VERSION;
}
ModelPersistence.saveModel(((SModelBase) model).getSModel(), (StreamDataSource) dataSource, persistenceVersion);
}
@Override
public void index(@NotNull InputStream input, @NotNull Callback callback) throws IOException {
ModelPersistence.index(input, callback);
}
@Override
public SModelData parseSingleStream(@NotNull String name, @NotNull InputStream input) throws IOException {
return ModelPersistence.getModelData(input);
}
@Override
public boolean isBinary() {
return false;
}
@Override
public String getFileExtension() {
return MPSExtentions.MODEL;
}
@NotNull
@Override
public String getFormatTitle() {
return "Universal XML-based format";
}
@NotNull
@Override
public ModelFactoryType getType() {
return PreinstalledModelFactoryTypes.PLAIN_XML;
}
@NotNull
@Override
public List<DataSourceType> getPreferredDataSourceTypes() {
return Collections.singletonList(PreinstalledDataSourceTypes.MPS);
}
public static Map<String, String> getDigestMap(@NotNull MultiStreamDataSource source, String streamName) {
InputStream is = null;
try {
is = source.openInputStream(streamName);
return getDigestMap(new InputStreamReader(is, FileUtil.DEFAULT_CHARSET));
} catch (IOException e) {
/* ignore */
} finally {
FileUtil.closeFileSafe(is);
}
return null;
}
public static Map<String, String> getDigestMap(@NotNull StreamDataSource source) {
InputStream is = null;
try {
is = source.openInputStream();
return getDigestMap(new InputStreamReader(is, FileUtil.DEFAULT_CHARSET));
} catch (IOException e) {
/* ignore */
} finally {
FileUtil.closeFileSafe(is);
}
return null;
}
public static Map<String, String> getDigestMap(Reader input) {
try {
return ModelPersistence.calculateHashes(FileUtil.read(input));
} catch (ModelReadException e) {
return null;
}
}
/**
* hack, @see BinaryModelPersistence#createFromHeader for details
*/
public static SModel createFromHeader(@NotNull SModelHeader header, @NotNull StreamDataSource dataSource) {
final ModelFactory modelFactory = PersistenceFacade.getInstance().getModelFactory(MPSExtentions.MODEL);
assert modelFactory instanceof DefaultModelPersistence;
return new DefaultSModelDescriptor(new PersistenceFacility((DefaultModelPersistence) modelFactory, dataSource), header.createCopy());
}
private static class PersistenceFacility extends LazyLoadFacility {
/*package*/ PersistenceFacility(DefaultModelPersistence modelFactory, StreamDataSource dataSource) {
super(modelFactory, dataSource);
}
@NotNull
@Override
public StreamDataSource getSource() {
return (StreamDataSource) super.getSource();
}
@Override
public Map<String, String> getGenerationHashes() {
Map<String, String> generationHashes = ModelDigestHelper.getInstance().getGenerationHashes(getSource());
if (generationHashes != null) {
return generationHashes;
}
return DefaultModelPersistence.getDigestMap(getSource());
}
@NotNull
@Override
public SModelHeader readHeader() throws ModelReadException {
return ModelPersistence.loadDescriptor(getSource());
}
@NotNull
@Override
public ModelLoadResult readModel(@NotNull SModelHeader header, ModelLoadingState state) throws ModelReadException {
return ModelPersistence.readModel(header, getSource(), state);
}
@Override
public boolean doesSaveUpgradePersistence(@NotNull SModelHeader header) {
//not sure !=-1 is really needed, just left to be ensured about compatibility
return header.getPersistenceVersion() != ModelPersistence.LAST_VERSION && header.getPersistenceVersion() != -1;
}
@Override
public void saveModel(@NotNull SModelHeader header, SModelData modelData) throws IOException {
ModelPersistence.saveModel((jetbrains.mps.smodel.SModel) modelData, getSource(), header.getPersistenceVersion());
}
}
}