/*
* 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.smodel.persistence.def;
import jetbrains.mps.persistence.FilePerRootDataSource;
import jetbrains.mps.smodel.DefaultSModel;
import jetbrains.mps.smodel.SModel;
import jetbrains.mps.smodel.SModelHeader;
import jetbrains.mps.smodel.SNodeId.Regular;
import jetbrains.mps.smodel.loading.ModelLoadResult;
import jetbrains.mps.smodel.loading.ModelLoadResult.ContentKind;
import jetbrains.mps.smodel.loading.ModelLoadingState;
import jetbrains.mps.util.FileUtil;
import jetbrains.mps.util.JDOMUtil;
import jetbrains.mps.util.xml.XMLSAXHandler;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jdom.Document;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.persistence.MultiStreamDataSource;
import org.xml.sax.InputSource;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* evgeny, 6/3/13
*/
public class FilePerRootFormatUtil {
private static final Logger LOG = LogManager.getLogger(FilePerRootFormatUtil.class);
public static SModelHeader loadDescriptor(MultiStreamDataSource dataSource) throws ModelReadException {
InputStream in = null;
try {
in = dataSource.openInputStream(FilePerRootDataSource.HEADER_FILE);
InputSource source = new InputSource(new InputStreamReader(in, FileUtil.DEFAULT_CHARSET));
return ModelPersistence.loadDescriptor(source);
} catch (IOException e) {
throw new ModelReadException("Couldn't read descriptor from " + dataSource.getLocation() + ": " + e.getMessage(), e);
} finally {
FileUtil.closeFileSafe(in);
}
}
public static ModelLoadResult readModel(SModelHeader header, MultiStreamDataSource dataSource, ModelLoadingState targetState) throws ModelReadException {
IModelPersistence mp;
int persistenceVersion = header.getPersistenceVersion();
if (!ModelPersistence.isSupported(persistenceVersion) || (mp = ModelPersistence.getPersistence(persistenceVersion)) == null) {
String m = "Can not find appropriate persistence version for model %s (requested:%d)\n Use newer version of JetBrains MPS to load this model.";
throw new PersistenceVersionNotFoundException(String.format(m, persistenceVersion, header.getModelReference()), header.getModelReference());
}
// load .model file
DefaultSModel result;
XMLSAXHandler<ModelLoadResult> headerHandler = mp.getModelReaderHandler(targetState, header);
InputStream in = null;
try {
in = dataSource.openInputStream(FilePerRootDataSource.HEADER_FILE);
InputSource source = new InputSource(new InputStreamReader(in, FileUtil.DEFAULT_CHARSET));
ModelPersistence.parseAndHandleExceptions(source, headerHandler);
if (headerHandler.getResult().getContentKind() != ContentKind.MODEL_HEADER) {
throw new ModelReadException("Couldn't read model: .model file is broken", null);
}
} catch (Exception e) {
Throwable th = e.getCause() == null ? e : e.getCause();
throw new ModelReadException(String.format("Couldn't read .model file: %s", th.getMessage()), e, header);
} finally {
FileUtil.closeFileSafe(in);
}
result = (DefaultSModel) headerHandler.getResult().getModel();
header = result.getSModelHeader();
// load roots
List<String> streams = new ArrayList<String>();
for (String s : dataSource.getAvailableStreams()) streams.add(s);
Collections.sort(streams);
for (String stream : streams) {
if (!(stream.endsWith(FilePerRootDataSource.ROOT_EXTENSION))) continue;
XMLSAXHandler<ModelLoadResult> rootHandler = mp.getModelReaderHandler(targetState, header);
in = null;
try {
in = dataSource.openInputStream(stream);
InputSource source = new InputSource(new InputStreamReader(in, FileUtil.DEFAULT_CHARSET));
ModelPersistence.parseAndHandleExceptions(source, rootHandler);
if (rootHandler.getResult().getContentKind() != ContentKind.MODEL_ROOT) {
throw new ModelReadException("Couldn't read model: " + stream + " root file is broken", null);
}
if (rootHandler.getResult().getState() == ModelLoadingState.INTERFACE_LOADED) {
headerHandler.getResult().setState(ModelLoadingState.INTERFACE_LOADED);
}
int count = 0;
SModel model = rootHandler.getResult().getModel();
model.enterUpdateMode();
for (SNode rootNode : model.getRootNodes()) {
if (count != 0) {
throw new ModelReadException(String.format("Couldn't read model from stream %s: root file is broken - contains more than one roots", stream), null);
}
count++;
// detach it from its spurious model, which is just a container for this single root
model.removeRootNode(rootNode);
// now that it's detached we can safely add it to our model
result.addRootNode(rootNode);
}
model.leaveUpdateMode();
} catch (Exception e) {
Throwable th = e.getCause() == null ? e : e.getCause();
throw new ModelReadException(String.format("Couldn't read model from stream %s: %s", stream, th.getMessage()), th, header);
} finally {
FileUtil.closeFileSafe(in);
}
}
return headerHandler.getResult();
}
public static int actualPersistenceVersion(int desiredPersistenceVersion) {
IModelPersistence modelPersistence = ModelPersistence.getPersistence(Math.max(desiredPersistenceVersion, ModelPersistence.FIRST_SUPPORTED_VERSION));
if (modelPersistence == null) {
modelPersistence = ModelPersistence.getPersistence(ModelPersistence.LAST_VERSION);
}
return modelPersistence.getVersion();
}
/**
* returns true if the content should be reloaded from storage after save
*/
public static boolean saveModel(SModel modelData, MultiStreamDataSource source, int persistenceVersion) throws IOException {
persistenceVersion = actualPersistenceVersion(persistenceVersion);
// upgrade?
SModelHeader modelHeader = null;
int oldVersion = persistenceVersion;
if (modelData instanceof DefaultSModel) {
DefaultSModel dsm = (DefaultSModel) modelData;
modelHeader = dsm.getSModelHeader();
oldVersion = modelHeader.getPersistenceVersion();
if (oldVersion != persistenceVersion) {
modelHeader.setPersistenceVersion(persistenceVersion);
}
}
// save into JDOM
if (persistenceVersion < 9) {
modelData.getImplicitImportsSupport().calculateImplicitImports();
}
Map<String, Document> result = ModelPersistence.getPersistence(persistenceVersion).getModelWriter(modelHeader).saveModelAsMultiStream(modelData);
// write to storage
Set<String> toRemove = new HashSet<String>();
for (String s : source.getAvailableStreams()) {
if (!result.containsKey(s)) toRemove.add(s);
}
for (Entry<String, Document> entry : result.entrySet()) {
//if we have a file having a name, which differs in case only, we want to remove this file before writing to the new one
//to sync cases in root- and filenames
String fnameLower = entry.getKey().toLowerCase();
Set<String> removed = new HashSet<String>();
for (String s : toRemove) {
if (s.toLowerCase().equals(fnameLower)){
source.delete(s);
removed.add(s);
}
}
toRemove.removeAll(removed);
JDOMUtil.writeDocument(entry.getValue(), source, entry.getKey());
}
for (String r : toRemove) {
source.delete(r);
}
if (oldVersion != persistenceVersion) {
LOG.info("persistence upgraded: " + oldVersion + "->" + persistenceVersion + " " + modelData.getReference());
return true;
}
return false;
}
public static Map<SNodeId, String> getStreamNames(SModel model) {
Map<SNodeId, String> result = new HashMap<SNodeId, String>();
Set<String> usedNames = new HashSet<String>();
for (SNode root : model.getRootNodes()) {
SNodeId key = root.getNodeId();
String value = asFileName(root.getName());
if (value.length() == 0) {
value = key instanceof Regular ? key.toString() : "Root";
}
if (!usedNames.add(value.toLowerCase())) {
String baseString = value;
int index = 2;
value = baseString + index;
while (!usedNames.add(value.toLowerCase())) {
index++;
value = baseString + index;
}
}
result.put(key, value + "." + FilePerRootDataSource.ROOT_EXTENSION);
}
return result;
}
private static String asFileName(String s) {
if (s == null) return "";
StringBuilder sb = new StringBuilder(s.length());
for (int i = 0; i < s.length(); i++) {
int c = (int) s.charAt(i);
if (c < 32) continue;
if (c >= 127 && !Character.isLetterOrDigit(c)) {
sb.append(Character.isWhitespace(c) ? ' ' : '_');
continue;
}
switch (c) {
case '\\':
case '/':
case ':':
case '"':
case '*':
case '?':
case '<':
case '>':
case '|':
case '#':
sb.append("_");
continue;
}
sb.append((char) c);
}
return sb.toString().trim();
}
}