/* * 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.project.structure; import jetbrains.mps.classloading.ClassLoaderManager; import jetbrains.mps.classloading.MPSClassesListener; import jetbrains.mps.classloading.MPSClassesListenerAdapter; import jetbrains.mps.extapi.model.GeneratableSModel; import jetbrains.mps.extapi.module.SModuleBase; import jetbrains.mps.generator.ModelDigestUtil; import jetbrains.mps.module.ReloadableModuleBase; import jetbrains.mps.project.persistence.LanguageDescriptorPersistence; import jetbrains.mps.smodel.BootstrapLanguages; import jetbrains.mps.smodel.Language; import jetbrains.mps.smodel.ModuleRepositoryFacade; import jetbrains.mps.smodel.SModelId.IntegerSModelId; import jetbrains.mps.smodel.SModelStereotype; import jetbrains.mps.smodel.SnapshotModelData; import jetbrains.mps.smodel.TrivialModelDescriptor; import jetbrains.mps.smodel.adapter.MetaAdapterByDeclaration; import jetbrains.mps.smodel.language.LanguageAspectSupport; import jetbrains.mps.util.JDOMUtil; import jetbrains.mps.util.MacrosFactory; import jetbrains.mps.vfs.IFile; import org.apache.log4j.Logger; import org.jdom.Document; import org.jdom.Element; import org.jetbrains.annotations.NotNull; import org.jetbrains.mps.openapi.event.SNodeAddEvent; import org.jetbrains.mps.openapi.event.SNodeRemoveEvent; import org.jetbrains.mps.openapi.language.SLanguage; import org.jetbrains.mps.openapi.model.EditableSModel; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SModelId; import org.jetbrains.mps.openapi.model.SModelName; import org.jetbrains.mps.openapi.model.SModelReference; import org.jetbrains.mps.openapi.model.SNodeChangeListenerAdapter; import org.jetbrains.mps.openapi.module.SModule; import java.io.ByteArrayOutputStream; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * Contributes '@descriptor' model to Language modules. */ public class LanguageDescriptorModelProvider extends DescriptorModelProvider { private final static SModelId ourDescriptorModelId = new IntegerSModelId(0x0f010101); private final Map<SModelReference, LanguageModelDescriptor> myModels = new ConcurrentHashMap<SModelReference, LanguageModelDescriptor>(); private final ClassLoaderManager myClassLoaderManager; private final RootChangeListener myListener = new RootChangeListener(); private class RootChangeListener extends SNodeChangeListenerAdapter { private final Set<SModelReference> myListenedModels = new HashSet<SModelReference>(); public void attach(SModule module) { for (SModel model : module.getModels()) { if (model instanceof EditableSModel && LanguageAspectSupport.isAspectModel(model)) { if (myListenedModels.add(model.getReference())) { model.addChangeListener(this); } } } } public void detach(SModule module) { // doesn't hurt to remove a listener even if we didn't add it for (SModel m : module.getModels()) { myListenedModels.remove(m.getReference()); m.removeChangeListener(this); } } @Override public void nodeAdded(@NotNull SNodeAddEvent event) { if (!event.isRoot()) { return; } Language language = Language.getLanguageFor(event.getModel()); if (language != null) { refreshModule(language,true); } } @Override public void nodeRemoved(@NotNull SNodeRemoveEvent event) { if (!event.isRoot()) { return; } Language language = Language.getLanguageFor(event.getModel()); if (language != null) { refreshModule(language,true); } } } private final MPSClassesListener myAspectReloadListener = new MPSClassesListenerAdapter() { @Override public void afterClassesLoaded(Set<? extends ReloadableModuleBase> loadedModules) { for (Language l : ModuleRepositoryFacade.getInstance().getAllModules(Language.class)) { aspects: for (SModel aspect : LanguageAspectSupport.getAspectModels(l)) { List<SLanguage> mainLanguages = new ArrayList<>(LanguageAspectSupport.getMainLanguages(aspect)); for (SModule loadedModule : loadedModules) { if (loadedModule instanceof Language) { if (mainLanguages.contains(MetaAdapterByDeclaration.getLanguage(((Language) loadedModule)))) { SModelReference ref = getSModelReference(l); LanguageModelDescriptor languageModelDescriptor = myModels.get(ref); if (languageModelDescriptor != null) { languageModelDescriptor.updateGenerationLanguages(); } break aspects; } } } } } } }; public LanguageDescriptorModelProvider(ClassLoaderManager classLoaderManager) { myClassLoaderManager = classLoaderManager; myClassLoaderManager.addClassesHandler(myAspectReloadListener); } @Override public void dispose() { myClassLoaderManager.removeClassesHandler(myAspectReloadListener); removeAll(); } /** * We don't care to supply descriptor model for deployed modules as there's no use for language descriptor there */ @Override public boolean isApplicable(SModule module) { return module instanceof Language && !module.isPackaged(); } @Override public void forgetModule(SModule language) { myListener.detach(language); Language module = (Language) language; SModelReference ref = getSModelReference(module); LanguageModelDescriptor descriptor = myModels.remove(ref); if (descriptor != null) { removeModel(descriptor); } } @Override public void refreshModule(SModule language) { refreshModule(language,false); } public void refreshModule(SModule language,boolean nodeChange) { myListener.attach(language); Language module = (Language) language; SModelReference ref = getSModelReference(module); if (!myModels.containsKey(ref)) { createModel(ref, module); } else { if (!nodeChange){ myModels.get(ref).updateGenerationLanguages(); } LanguageModelDescriptor languageModelDescriptor = myModels.get(ref); if (languageModelDescriptor != null) { languageModelDescriptor.invalidate(); } } } private void removeAll() { List<LanguageModelDescriptor> models = new ArrayList<LanguageModelDescriptor>(myModels.values()); for (LanguageModelDescriptor model : models) { removeModel(model); } myModels.clear(); } private void removeModel(LanguageModelDescriptor md) { SModule module = md.getModule(); if (module instanceof SModuleBase) { ((SModuleBase) module).unregisterModel(md); } } public LanguageModelDescriptor createModel(SModelReference ref, @NotNull Language module) { LanguageModelDescriptor result = new LanguageModelDescriptor(ref, module); result.updateGenerationLanguages(); myModels.put(ref, result); module.registerModel(result); return result; } /*package*/ static SModelReference getSModelReference(Language module) { return new jetbrains.mps.smodel.SModelReference(module.getModuleReference(), ourDescriptorModelId, new SModelName(module.getModuleName(), SModelStereotype.DESCRIPTOR)); } public String toString() { return "component: Language Descriptor Models Provider"; } public static final class LanguageModelDescriptor extends TrivialModelDescriptor implements GeneratableSModel { private final Language myModule; private String myHash; /* * Module file keeps closure of its dependencies, and the change in the closure is not propagated as a module changed event. * (e.g. if used devkit got new exported solution, version of the solution module is recorded under dependencyVersions tag) * Without module change, hash has not been re-calculated and no 'generation required' status show up. To mitigate, * record timestamp of a module file the moment hash is calculated. */ private long myHashTimestamp; private LanguageModelDescriptor(SModelReference ref, Language module) { super(new SnapshotModelData(ref)); myModule = module; myHash = null; } /** * FIXME * adding used languages to descriptor model is a hack, * fixing that the runtime solutions of languages engaged on generations are ignored at compilation */ void updateGenerationLanguages() { jetbrains.mps.smodel.SModel m = getSModel(); m.addDevKit(BootstrapLanguages.getLanguageDescriptorDevKit()); m.addEngagedOnGenerationLanguage(BootstrapLanguages.getLanguageDescriptorLang()); Set<SLanguage> importsToRemove = new HashSet<>(m.usedLanguages()); // calculating the delta Set<SLanguage> importsToAdd = new HashSet<>(); Collection<SModel> aspectModels = LanguageAspectSupport.getAspectModels(myModule); for (SModel aspect : aspectModels) { for (@NotNull SLanguage aspectLanguage : LanguageAspectSupport.getMainLanguages(aspect)) { addEngagedOnGenerationLanguage(aspectLanguage); importsToRemove.remove(aspectLanguage); importsToAdd.add(aspectLanguage); } } importsToAdd.removeAll(m.usedLanguages()); // not adding the same language again importsToRemove.forEach(m::deleteLanguage); // applying calculated delta importsToAdd.forEach(m::addLanguage); for (SLanguage lang : m.usedLanguages()) { int versionFromModule = myModule.getUsedLanguageVersion(lang, false); if (m.getLanguageImportVersion(lang) != versionFromModule) { m.setLanguageImportVersion(lang, versionFromModule); } } } @Override public boolean isGeneratable() { return !myModule.isReadOnly(); } @Override public boolean isGenerateIntoModelFolder() { return false; } @Override public void setGenerateIntoModelFolder(boolean value) { throw new UnsupportedOperationException(); } @Override public String getModelHash() { String hash = myHash; IFile descriptorFile = myModule.getDescriptorFile(); long hashTimestamp = descriptorFile.lastModified(); if (hash != null && hashTimestamp == myHashTimestamp) { return hash; } try { ByteArrayOutputStream output = new ByteArrayOutputStream(); Element xmlElement = new LanguageDescriptorPersistence(MacrosFactory.forModuleFile(descriptorFile)).save(myModule.getModuleDescriptor()); JDOMUtil.writeDocument(new Document(xmlElement), output); hash = ModelDigestUtil.hashText(output.toString()); } catch (Exception ex) { Logger.getLogger(LanguageDescriptorModelProvider.class).error("Failed to detect changes in a module descriptor", ex); return null; } BigInteger modelHash = new BigInteger(hash, Character.MAX_RADIX); for (SModel aspModel : LanguageAspectSupport.getAspectModels(myModule)) { if (aspModel instanceof EditableSModel && !((EditableSModel) aspModel).isChanged() && aspModel instanceof GeneratableSModel) { modelHash = modelHash.xor(new BigInteger(((GeneratableSModel) aspModel).getModelHash(), Character.MAX_RADIX)); } } hash = modelHash.toString(Character.MAX_RADIX); myHash = hash; myHashTimestamp = hashTimestamp; return hash; } @Override public Map<String, String> getGenerationHashes() { return Collections.singletonMap(GeneratableSModel.FILE, getModelHash()); } @Override public void setDoNotGenerate(boolean value) { throw new UnsupportedOperationException(); } @Override public boolean isDoNotGenerate() { return false; } public void invalidate() { if (getSModel().isDisposed()) { // SModelBase.detach() dispose a model, but doesn't null the reference. // When we delete a language module, models are deleted one by one, and if @descriptor is deleted first, // beforeRemove(other models) fails with NPE on update to change reference of disposed model // Not sure though, if it's the right approach, if we won't get to invalidate() with disposed descriptor, but // there is a need to re-init descriptor model. return; } changeModelReference(getSModelReference(myModule)); myHash = null; } } }