/*
* Copyright 2003-2017 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.generator.impl.plan;
import jetbrains.mps.extapi.model.SModelBase;
import jetbrains.mps.generator.generationTypes.StreamHandler;
import jetbrains.mps.generator.impl.MappingLabelExtractor;
import jetbrains.mps.generator.impl.ModelStreamManager;
import jetbrains.mps.generator.impl.SingleStreamSource;
import jetbrains.mps.generator.impl.cache.MappingsMemento;
import jetbrains.mps.generator.plan.CheckpointIdentity;
import jetbrains.mps.generator.plan.PlanIdentity;
import jetbrains.mps.smodel.persistence.def.ModelPersistence;
import jetbrains.mps.util.JDOMUtil;
import org.apache.log4j.Logger;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.persistence.ModelFactory;
import org.jetbrains.mps.openapi.persistence.PersistenceFacade;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.stream.StreamSupport;
/**
* Container for checkpoint models, access to deployed/available models.
* {@code {@link CheckpointVault}} is one per model and populates single {@link ModelCheckpoints} object with persisted
* state of checkpoints for the given model (model identified with {@link ModelStreamManager}.
* Unlike {@link ModelCheckpoints}, this is focused on checkpoint SModel persistence, rather than
* API suited for reference resolution.
* Since it is a vault for all CPs of a given model, map model-to-ModelCheckpoints is kept elsewhere (now {@link CrossModelEnvironment})
* XXX ideas:
* read models into no module/repo here, populate ModelCheckpoints object (no need to obtain read access!)
* then, once checkpoints are needed, they get exposed in new transient module (with name that resembles name
* of the original module, just in case anyone needs it?).
* 1. Multiple plans with few CPs per model - how to persist.
* 2. When to persist? TextGenToMemory shall not serialize it, while regular TextGen shall.
* Both shall read CPs if available
* 3. How to manage removal of CPs/stale CPs?
* 4. --
* 5. Files: checkpoints [1] + planName-cpName.mps [*], former lists all known .mps files with cp models.
* What about conflicts? Another file extension? UUID file names? On one hand, don't need them human-readable
* OTOH, when it's easy to recognize where file belongs is nice
* 6. Do I need to reconstruct ModelCheckpoints in RT, without serializing the registry)? Perhaps, I don't need to
* keep identity of checkpoint and plan inside a model (now with facilities of ModelWithAttributes)
* 7. What about consistency of plan and its persisted cp models? What if Plan is expected to have CP1 and CP2, but locally
* we've got CP1 and CP3? How do we check consistency, how do we react to inconsistency.
*
* <p/>
* Pretty much similar to {@code ExportsVault} and conceptually to {@code ModelVault}, perhaps
* a shared superclass worth adding. The difference is that both aforementioned vaults deal with
* single associated stream/model, while here we need to deal with multiple checkpoint models.
* @author Artem Tikhomirov
* @since 3.4
*/
public class CheckpointVault {
private final ModelStreamManager myStreams;
private final List<Entry> myKnownCheckpoints;
private ModelCheckpoints myCheckpoints;
public CheckpointVault(@NotNull ModelStreamManager modelStreams) {
myStreams = modelStreams;
myKnownCheckpoints = new ArrayList<>();
}
@Nullable
/*package*/ ModelCheckpoints getCheckpointsFor(@NotNull BiFunction<SModel, CheckpointIdentity, SModel> publisher) {
if (myCheckpoints == null) {
ArrayList<CheckpointState> states = new ArrayList<>(myKnownCheckpoints.size());
for (Entry entry : myKnownCheckpoints) {
try {
CheckpointState cpState = loadModel(entry, publisher);
states.add(cpState);
} catch (IOException ex) {
// FIXME fail quietly for now, think over better error handling
// - fail fast with exception?
// - pre-check all required CPs are present at Make
// - load all CP at the generation start?
// - FIXME why not report error through IMessageHandler so that client could see it? Warning, perhaps?
String msg = String.format("Failed to load model for checkpoint %s from %s", entry.myCheckpoint, entry.myFile);
Logger.getLogger(CheckpointVault.class).error(msg, ex);
}
}
// myCheckpoints != null indicates we would NOT attempt to load next time. Is it worth to consider keep trying?
myCheckpoints = new ModelCheckpoints(states);
}
return myCheckpoints;
}
/**
* Replace matching, add new checkpoints, do not touch persisted for non-matching cp identities.
* @param mcp new checkpoints
*/
public void updateCheckpointsOf(@NotNull ModelCheckpoints mcp) {
ArrayList<Entry> newEntries = new ArrayList<>(4);
for (CheckpointIdentity next : mcp.getKnownCheckpoints()) {
Entry existing = null;
for (Entry entry : myKnownCheckpoints) {
if (next.equals(entry.myCheckpoint)) {
existing = entry;
break;
}
}
// respect filename of CP if known, add blank names for missing CPs only
if (existing == null) {
newEntries.add(existing = new Entry(next, null));
}
existing.myChangedState = mcp.find(next);
}
myKnownCheckpoints.addAll(newEntries);
myCheckpoints = null;
}
/**
* read xml that lists all checkpoint models from all generation plans for the given model
*/
/*package*/ void readCheckpointRegistry() {
try {
myKnownCheckpoints.clear();
try (InputStream is = myStreams.getOutputLocation().openInputStream("checkpoints")) {
Document cpDoc = JDOMUtil.loadDocument(is);
for (Element planElement : cpDoc.getRootElement().getChildren("plan")) {
PlanIdentity pi = new PlanIdentity(planElement.getAttributeValue("id"));
for (Element cpElement : planElement.getChildren("checkpoint")) {
// XXX shall I check for duplicates (same cpId or file name?)
CheckpointIdentity cpId = new CheckpointIdentity(pi, cpElement.getAttributeValue("id"));
// perhaps, shall record model ref of CP model not to read file if we need just ref.
String file = cpElement.getAttributeValue("file");
assert file != null;
myKnownCheckpoints.add(new Entry(cpId, file));
}
}
}
} catch (FileNotFoundException ex) {
Logger.getLogger(GenerationPlan.class).debug("No checkpoint registry file found");
} catch (IOException | JDOMException ex) {
Logger.getLogger(GenerationPlan.class).warn("Failed to read checkpoint registry", ex);
}
}
private Element buildCheckpointRegistry() {
Element root = new Element("checkpoints");
// FIXME with no plan grouping, introduce a new format, with distinct cp entries, not grouped under <plan>
for (Entry entry : myKnownCheckpoints) {
Element planElement = new Element("plan");
planElement.setAttribute("id", entry.myCheckpoint.getPlan().getName());
Element cpElement = new Element("checkpoint");
cpElement.setAttribute("id", entry.myCheckpoint.getName());
// FIXME ensure names are unique
cpElement.setAttribute("file", entry.getFilename());
planElement.addContent(cpElement);
root.addContent(planElement);
}
return root;
}
private CheckpointState loadModel(Entry entry, BiFunction<SModel, CheckpointIdentity, SModel> publisher) throws IOException {
final ModelFactory modelFactory = PersistenceFacade.getInstance().getDefaultModelFactory();
final SingleStreamSource source = new SingleStreamSource(myStreams.getOutputLocation(), entry.getFilename());
SModel cpModel = modelFactory.load(source, Collections.emptyMap());
MappingsMemento memento = new MappingLabelExtractor().restore(MappingLabelExtractor.findDebugNode(cpModel));
return new CheckpointState(memento, publisher.apply(cpModel, entry.myCheckpoint), entry.myCheckpoint);
}
// FIXME use of StreamHandler;
// XXX is it possible to get more than 1 Entry changed?
/*package*/ void saveChanged(StreamHandler handler) {
if (StreamSupport.stream(myKnownCheckpoints.spliterator(), false).noneMatch(e -> e.myChangedState != null)) {
return;
}
Element cpRegistry = buildCheckpointRegistry();
// FIXME it's bad to use different sets of API (StreamProvider vs StreamHandler) to read/write CPs.
handler.saveStream("checkpoints", cpRegistry);
for (Entry entry : myKnownCheckpoints) {
if (entry.myChangedState == null) {
continue;
}
// buildCheckpointRegistry() above ensures we've got all file names;
CheckpointState cpState = entry.myChangedState;
// FIXME use ModelFactory.save(cpState.getCheckpointModel(), InMemoryDataSource) instead
Document d = ModelPersistence.saveModel(((SModelBase) cpState.getCheckpointModel()).getSModel());
handler.saveStream(entry.getFilename(), d.getRootElement());
}
}
private static class Entry {
/*package*/ final CheckpointIdentity myCheckpoint;
private String myFile;
/*package*/ CheckpointState myChangedState; // non-null value indicates checkpoint model was updated and need save
public Entry(CheckpointIdentity cp, String file) {
myCheckpoint = cp;
myFile = file;
}
private String getFilename() {
if (myFile == null) {
StringBuilder fname = new StringBuilder();
fname.append(myCheckpoint.getPlan().getPersistenceValue());
fname.append('-');
fname.append(myCheckpoint.getPersistenceValue());
fname.append(".mps");
myFile = fname.toString();
}
return myFile;
}
}
}