/*
* 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;
import jetbrains.mps.generator.GenerationOptions.OptionsBuilder;
import jetbrains.mps.generator.impl.GenPlanTranslator;
import jetbrains.mps.generator.impl.plan.EngagedGeneratorCollector;
import jetbrains.mps.generator.impl.plan.RegularPlanBuilder;
import jetbrains.mps.messages.IMessageHandler;
import jetbrains.mps.messages.Message;
import jetbrains.mps.messages.MessageKind;
import jetbrains.mps.project.DevKit;
import jetbrains.mps.smodel.SModelInternal;
import jetbrains.mps.smodel.language.LanguageRegistry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleFacet;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* For a given model, figure out generation plan associated either with module's custom facet or through devkit
* and populate generator options appropriately.
* @implNote doesn't address model read. may cache information about plans found
* @author Artem Tikhomirov
* @since 3.4
*/
public final class GenPlanExtractor implements ModelGenerationPlan.Provider {
private final SRepository myRepository;
private final OptionsBuilder myOptions;
private final Map<SModule, ModelGenerationPlan.Provider> myOwnerModuleToFacet = new HashMap<>();
private final Set<SModule> myOwnerModulesNoCustomFacet = new HashSet<>();
// null value indicates there's no plan associated with devkit (or the plan couldn't get instantiated).
private final Map<SModuleReference, PlanProviderInfo> myDevkitToPlan = new HashMap<>();
private final IMessageHandler myMessageHandler;
public GenPlanExtractor(@NotNull SRepository repository, @Nullable IMessageHandler messageHandler) {
myRepository = repository;
myOptions = null;
myMessageHandler = messageHandler == null ? IMessageHandler.NULL_HANDLER : messageHandler;
}
public GenPlanExtractor(@NotNull SRepository repository, @NotNull GenerationOptions.OptionsBuilder options, @Nullable IMessageHandler messageHandler) {
myRepository = repository;
myOptions = options;
myMessageHandler = messageHandler == null ? IMessageHandler.NULL_HANDLER : messageHandler;
}
/**
*
* @param model model being prepared for generation
* @return <code>true</code> when there's custom generation plan associated with the model.
*/
public boolean hasPlan(@NotNull SModel model) {
return planFromCustomFacet(model) != null || planFromDevKit(model) != null;
}
@Nullable
private ModelGenerationPlan planFromCustomFacet(SModel model) {
final SModule ownerModule = model.getModule();
final ModelGenerationPlan.Provider facet = myOwnerModuleToFacet.get(ownerModule);
if (facet != null) {
return facet.getPlan(model);
}
if (!myOwnerModulesNoCustomFacet.contains(ownerModule)) {
// ok, it's the first time we see the module
ModelGenerationPlan.Provider f = fromModuleFacets(ownerModule);
if (f != null) {
myOwnerModuleToFacet.put(ownerModule, f);
return f.getPlan(model);
} else {
myOwnerModulesNoCustomFacet.add(ownerModule);
// fall-through
}
}
return null;
}
private static ModelGenerationPlan.Provider fromModuleFacets(SModule module) {
for (SModuleFacet mf : module.getFacets()) {
if (mf instanceof ModelGenerationPlan.Provider) {
return (ModelGenerationPlan.Provider) mf;
}
}
return null;
}
/**
* First, look if any devkit specifies GP. First DevKit with associated plan is consulted, if any, and no further lookup is done.
* If there are no devkits with associated plans, check facets of devkit modules if any is an MGP.Provider. First facet found serves as provider then.
* @param model transformed model, the one we need plan for
* @return GP's API instance
*/
@Nullable
private ModelGenerationPlan planFromDevKit(SModel model) {
// plans associated directly with devkit property has higher precedence than plans coming from DevKit's facets plan providers
ArrayList<ModelGenerationPlan.Provider> facetAssociatedPlan = new ArrayList<>();
for (SModuleReference dkRef : ((SModelInternal) model).importedDevkits()) {
final SModelReference dkPlan;
if (myDevkitToPlan.containsKey(dkRef)) {
final PlanProviderInfo rv = myDevkitToPlan.get(dkRef);
if (rv == null) {
// we've seen this devkit and know it has no plan
continue;
}
if (rv.isDirect) {
return rv.provider.getPlan(model);
} else {
facetAssociatedPlan.add(rv.provider);
// FALL-THROUGH, continue;
}
} else {
final SModule dkModule = dkRef.resolve(myRepository);
if (!(dkModule instanceof DevKit)) {
continue;
}
DevKit devkit = (DevKit) dkModule;
ModelGenerationPlan.Provider mgpProvider;
if (devkit.getModuleDescriptor() != null && (dkPlan = devkit.getModuleDescriptor().getAssociatedGenPlan()) != null) {
mgpProvider = new InterpretedPlanProvider(dkPlan);
myDevkitToPlan.put(dkRef, new PlanProviderInfo(mgpProvider, true));
return mgpProvider.getPlan(model);
} else {
mgpProvider = fromModuleFacets(devkit);
if (mgpProvider != null) {
myDevkitToPlan.put(dkRef, new PlanProviderInfo(mgpProvider, false));
facetAssociatedPlan.add(mgpProvider);
} else {
myDevkitToPlan.put(dkRef, null);
}
}
}
}
//noinspection LoopStatementThatDoesntLoop
for (ModelGenerationPlan.Provider p : facetAssociatedPlan) {
// we can get here only if there's no GP directly associated with any imported devkit
return p.getPlan(model);
// I use collection of facet-associated plans though need only first one to
// (a) avoid complicated already-set check when looping through all imported DK in attempt to find
// (b) might want to compose plans from devkit module facets and consult all of them. I.e. if there are 3 devkits in a model, each with a
// facet/MGP.Provider, then I can ask all of them in order, to see if any could handle the model in question
// NOTE, I could do the same for 'primary' plans (associated directly with a DK), although now stick to first one in an attempt to
// make plan selection predictable.
}
return null;
}
/**
* @param model model being prepared for generation
* @return plan instance ready to get {@linkplain OptionsBuilder#customPlan(SModel, ModelGenerationPlan) associated} with the model
* @throws IllegalArgumentException if model supplied {@linkplain #hasPlan(SModel) has no plan}
*/
@NotNull
public ModelGenerationPlan getPlan(@NotNull SModel model) throws IllegalArgumentException {
ModelGenerationPlan rv = planFromCustomFacet(model);
if (rv != null) {
String m = String.format("Generation plan for model %s defined with a custom module facet", model.getName());
myMessageHandler.handle(new Message(MessageKind.INFORMATION, GenPlanExtractor.class, m));
return rv;
}
rv = planFromDevKit(model);
if (rv != null) {
String m = String.format("Generation plan for model %s defined with an employed devkit", model.getName());
myMessageHandler.handle(new Message(MessageKind.INFORMATION, GenPlanExtractor.class, m));
return rv;
}
assert !hasPlan(model) : "API consistency check";
throw new IllegalArgumentException(String.format("Model %s has no associated custom plan", model.getName()));
}
public void configurePlanFor(@NotNull SModel model) {
assert myOptions != null;
if (!hasPlan(model)) {
return;
}
ModelGenerationPlan p = getPlan(model);
myOptions.customPlan(model, p);
}
final class InterpretedPlanProvider implements ModelGenerationPlan.Provider {
private final SModelReference myPlanModelRef;
/*package*/ InterpretedPlanProvider(SModelReference planModelRef) {
myPlanModelRef = planModelRef;
}
@Nullable
@Override
public ModelGenerationPlan getPlan(@NotNull SModel model) {
final SModel planModel = myPlanModelRef.resolve(myRepository);
if (planModel == null) {
return null;
}
GenPlanTranslator gpt = new GenPlanTranslator(planModel.getRootNodes().iterator().next());
// FIXME in fact, shall respect additional languages passed through GenerationParametersProviderEx.getAdditionalLanguages(SModel), like
// original GenerationPlan did. However, it's rarely (if ever) used feature and contemporary GPs replace it completely, so I do not bother.
EngagedGeneratorCollector egc = new EngagedGeneratorCollector(model, null);
RegularPlanBuilder planBuilder = new RegularPlanBuilder(LanguageRegistry.getInstance(myRepository), egc.getGenerators(), myMessageHandler);
gpt.feed(planBuilder);
return planBuilder.wrapUp(gpt.getPlanIdentity());
}
}
final class PlanProviderInfo {
final boolean isDirect; // true if MGP is assocated with a devkit directly, false if comes through facets
final ModelGenerationPlan.Provider provider;
PlanProviderInfo(ModelGenerationPlan.Provider p, boolean direct) {
isDirect = direct;
provider = p;
}
}
}