/* * 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.modules; import jetbrains.mps.persistence.MementoImpl; import jetbrains.mps.project.AbstractModule; import jetbrains.mps.project.ModuleId; import jetbrains.mps.project.facets.JavaModuleFacet; import jetbrains.mps.project.structure.model.ModelRootDescriptor; import jetbrains.mps.util.annotation.ToRemove; import jetbrains.mps.util.io.ModelInputStream; import jetbrains.mps.util.io.ModelOutputStream; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.language.SLanguage; import org.jetbrains.mps.openapi.module.SModuleFacet; import org.jetbrains.mps.openapi.module.SModuleReference; import org.jetbrains.mps.openapi.module.SRepository; import org.jetbrains.mps.openapi.persistence.PersistenceFacade; import java.io.IOException; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.function.Supplier; /** * This class captures persistence and editing aspects of SModule. Client code shall not use this class * unless its purpose is to edit or persist module properties. Use SModule API (or Language/Generator/Solution/DevKit subclasses) * to read module dependencies and identity information. * * ----------------------------------------------------------------------------------------------------------------------------------- * FIXME This class mixes up the persistence and editing aspects of the {@link AbstractModule} class. * FIXME in order to edit facets/model roots in the module a client needs to access such entities as {@link ModuleFacetDescriptor}, {@link ModelRootDescriptor} directly, * FIXME when he has just an {@link AbstractModule} (which leads to a low-level module#getModuleDescriptor.getFacetDescriptors().add...) * FIXME obviously it is wrong: a client should rather work with {@link SModuleFacet} entities in the case of editing an {@link AbstractModule}, not descriptors. * FIXME OTOH it cannot be a plain persistence descriptor since in order to update (more or less) any properties of an {@link AbstractModule} * FIXME we use such pattern in the {@code AbstractModule} as: * <code> * AbstractModule module; * var descriptor = module.getDescriptor(); * <change descriptor freely as we wish> * module.setDescriptor(descriptor); // commit descriptor * </code> * which is needed in order to guarantee a consistency of the {@link AbstractModule} operations. * * TODO Also I would rather use in the ModuleDescriptor hierarchy composition instead of inheritance. The {@link #myDeploymentDescriptor} reference is especially repelling here. * * Road map: * We separate the persistence descriptor from the special editing 'handle'. * We ensure that the persistence descriptor reflects all the properties we find in our module persistence. * * AP */ public class ModuleDescriptor implements CopyableDescriptor<ModuleDescriptor> { private ModuleId myId; private String myNamespace; private String myTimestamp; private int myModuleVersion; private final Collection<ModelRootDescriptor> myModelRoots = new LinkedHashSet<>(); private final Collection<ModuleFacetDescriptor> myFacets = new LinkedHashSet<>(); private final Collection<Dependency> myDependencies = new LinkedHashSet<>(); private final Collection<SModuleReference> myUsedLanguages = new LinkedHashSet<>(); private final Collection<SModuleReference> myUsedDevkits = new LinkedHashSet<>(); private final Map<SLanguage, Integer> myLanguageVersions = new LinkedHashMap<>(); private final Map<SModuleReference, Integer> myDependencyVersions = new LinkedHashMap<>(); private final Collection<String> myAdditionalJavaStubPaths = new LinkedHashSet<>(); private final Collection<String> mySourcePaths = new LinkedHashSet<>(); private DeploymentDescriptor myDeploymentDescriptor; // FIXME must be removed private Throwable myLoadException; private boolean myUseTransientOutput; private boolean myHasLanguageVersions = true; private boolean myHasDependencyVersions = true; public ModuleDescriptor() { } public final ModuleId getId() { return myId; } public final void setId(ModuleId id) { myId = id; } public final String getNamespace() { return myNamespace; } public final void setNamespace(String namespace) { myNamespace = namespace; } public final SModuleReference getModuleReference() { return PersistenceFacade.getInstance().createModuleReference(getId(), getNamespace()); } public final String getTimestamp() { return myTimestamp; } public final void setTimestamp(String timestamp) { myTimestamp = timestamp; } public boolean getCompileInMPS() { throw new UnsupportedOperationException(); } public final Collection<ModelRootDescriptor> getModelRootDescriptors() { return myModelRoots; } public final Collection<ModuleFacetDescriptor> getModuleFacetDescriptors() { return myFacets; } /** * PROVISIONAL API, DO NOT USE OUTSIDE OF MPS * When facet is added/replaced in a module, we need to register it with persistence (module descriptor). * <p/> * With this methods, we keep facet persistence management in a single place (rather than * <code>facet.save(new Memento())</code> scattered around) */ public final void addFacetDescriptor(@NotNull SModuleFacet facet) { removeFacetDescriptor(facet); myFacets.add(new ModuleFacetDescriptor(facet.getFacetType(), new MementoImpl())); } /** * PROVISIONAL API, DO NOT USE OUTSIDE OF MPS * Forget persistence information for the given facet */ public final void removeFacetDescriptor(@NotNull SModuleFacet facet) { final String facetType = facet.getFacetType(); for (Iterator<ModuleFacetDescriptor> it = myFacets.iterator(); it.hasNext(); ) { ModuleFacetDescriptor facetDescriptor = it.next(); if (facetType.equals(facetDescriptor.getType())) { it.remove(); break; } } } /** * PROVISIONAL API, DO NOT USE OUTSIDE OF MPS * Push facet settings into persistence. * If there's no descriptor for the facet, it's ignored (use {@link #addFacetDescriptor(SModuleFacet)} first) * This behavior (no update for missing descriptors) is important as facet removal is three-fold - we remove descriptor first, * while facet at the module still active, then we update persistent values of existing facets, and then reload module with new descriptor. * If we would update missing descriptors, we would effectively resurrect removed descriptors. * <p/> * It's not clear whether this code shall be part of AbstractModule#save or not. It seems reasonable to * push facet settings into persistence on module save, OTOH, the way module settings are edited/updated (i.e. with * changes into descriptor and AbstractModule.setModuleDescriptor() + AM.save(), see ModulePropertiesConfigurable) * makes me feel update in the descriptor, not in the module, is better option (after all, it's ModuleDescriptor that is responsible * for editing of module settings) - it's sort of changes snapshot, applied with a single setModuleDescriptor operation, rather than sequence * of SModule changes. */ public final void updateFacetDescriptor(@NotNull SModuleFacet facet) { for (ModuleFacetDescriptor facetDescriptor : myFacets) { if (facetDescriptor.getType().equals(facet.getFacetType())) { facet.save(facetDescriptor.getMemento()); break; } } } public final Collection<Dependency> getDependencies() { return myDependencies; } /** * @deprecated Now, used languages of a module are derived from models it owns, and thus * this list is generally empty. Although generally there might be modules that keep their used languages, and ModuleDescriptor * may keep the list, it shall be the list of SLanguage anyway. */ @Deprecated @ToRemove(version = 3.3) public final Collection<SModuleReference> getUsedLanguages() { return myUsedLanguages; } public final Map<SLanguage, Integer> getLanguageVersions() { return myLanguageVersions; } public final Map<SModuleReference, Integer> getDependencyVersions() { return myDependencyVersions; } public final Collection<SModuleReference> getUsedDevkits() { return myUsedDevkits; } /** * Paths to extra jar files needed to compile and run given module, generally empty unless a module has some peculiar dependencies on existing java libraries. * As of today, these come from {@code <stubModelEntry path=""/>} in a module descriptor. * according to {@code LanguageDescriptorPersistence}, legacy entries were {@code classPath} and {@code runtimeClassPath} * FIXME WHY DOES IT USE String for File location, which FS shall I use to resolve these locations? */ public final Collection<String> getAdditionalJavaStubPaths() { return myAdditionalJavaStubPaths; } /** * Additional source files to compile along with module's own generated output. * Though, uses are bit odd: * - There's unused {@link AbstractModule#getSourcePaths()} * - JavaModuleFacet manifests these {@link JavaModuleFacet#getAdditionalSourcePaths()}, likely using module descriptor just as a storage (it's what JMF does anyway) * - Make respects these to compile a module */ public final Collection<String> getSourcePaths() { return mySourcePaths; } @Nullable public final DeploymentDescriptor getDeploymentDescriptor() { return myDeploymentDescriptor; } public final void setDeploymentDescriptor(DeploymentDescriptor deploymentDescriptor) { myDeploymentDescriptor = deploymentDescriptor; } public boolean updateModelRefs(SRepository repository) { return false; } public boolean updateModuleRefs(SRepository repository) { RefUpdateUtil uu = new RefUpdateUtil(repository); return RefUpdateUtil.composeUpdates( uu.updateModuleRefs(myUsedLanguages), uu.updateModuleRefs(myUsedDevkits), uu.updateDependencies(myDependencies) ); } public Throwable getLoadException() { return myLoadException; } public void setLoadException(Throwable loadException) { myLoadException = loadException; } public boolean isUseTransientOutput() { return myUseTransientOutput; } public void setUseTransientOutput(boolean useTransientOutput) { myUseTransientOutput = useTransientOutput; } protected int getHeaderMarker() { return 0x73048111; } public void save(ModelOutputStream stream) throws IOException { stream.writeInt(getHeaderMarker()); stream.writeModuleID(myId); stream.writeString(myNamespace); stream.writeString(myTimestamp); stream.writeInt(myModelRoots.size()); for (ModelRootDescriptor root : myModelRoots) { root.save(stream); } stream.writeInt(myFacets.size()); for (ModuleFacetDescriptor facet : myFacets) { facet.save(stream); } stream.writeInt(myDependencies.size()); for (Dependency dep : myDependencies) { dep.save(stream); } stream.writeInt(myUsedLanguages.size()); for (SModuleReference ref : myUsedLanguages) { stream.writeModuleReference(ref); } stream.writeInt(myUsedDevkits.size()); for (SModuleReference ref : myUsedDevkits) { stream.writeModuleReference(ref); } stream.writeStrings(myAdditionalJavaStubPaths); stream.writeStrings(mySourcePaths); stream.writeByte(myDeploymentDescriptor != null ? 0x1 : 0x70); if (myDeploymentDescriptor != null) { myDeploymentDescriptor.save(stream); } stream.writeBoolean(myUseTransientOutput); stream.writeInt(myModuleVersion); stream.writeByte(0x3a); } public void load(ModelInputStream stream) throws IOException { if (stream.readInt() != getHeaderMarker()) throw new IOException("bad stream: no module descriptor start marker"); myId = stream.readModuleID(); myNamespace = stream.readString(); myTimestamp = stream.readString(); myModelRoots.clear(); for (int size = stream.readInt(); size > 0; size--) { myModelRoots.add(ModelRootDescriptor.load(stream)); } myFacets.clear(); for (int size = stream.readInt(); size > 0; size--) { myFacets.add(ModuleFacetDescriptor.load(stream)); } myDependencies.clear(); for (int size = stream.readInt(); size > 0; size--) { Dependency dep = new Dependency(); dep.load(stream); myDependencies.add(dep); } myUsedLanguages.clear(); for (int size = stream.readInt(); size > 0; size--) { myUsedLanguages.add(stream.readModuleReference()); } myUsedDevkits.clear(); for (int size = stream.readInt(); size > 0; size--) { myUsedDevkits.add(stream.readModuleReference()); } myAdditionalJavaStubPaths.clear(); myAdditionalJavaStubPaths.addAll(stream.readStrings()); mySourcePaths.clear(); mySourcePaths.addAll(stream.readStrings()); byte b = stream.readByte(); if (b == 0x1) { myDeploymentDescriptor = new DeploymentDescriptor(); myDeploymentDescriptor.load(stream); } else if (b == 0x70) { myDeploymentDescriptor = null; } else { throw new IOException("broken stream"); } myUseTransientOutput = stream.readBoolean(); myModuleVersion = stream.readInt(); if (stream.readByte() != 0x3a) throw new IOException("bad stream: no module descriptor end marker"); } /** * @deprecated needed it for migration (3.1->3.2) */ @ToRemove(version = 3.4) @Deprecated public final void setHasLanguageVersions(boolean hasLanguageVersions) { myHasLanguageVersions = hasLanguageVersions; } /** * @deprecated needed it for migration (3.2->3.3) */ @ToRemove(version = 3.4) @Deprecated public final void setHasDependencyVersions(boolean hasDependencyVersions) { myHasDependencyVersions = hasDependencyVersions; } @Deprecated public final boolean hasLanguageVersions() { return myHasLanguageVersions; } @ToRemove(version = 3.4) @Deprecated public final boolean hasDependencyVersions() { return myHasDependencyVersions; } public final int getModuleVersion() { return myModuleVersion; } public final void setModuleVersion(int version) { myModuleVersion = version; } /** * utility method to help subclasses implementing the {@link #copy()} method * * @param concreteConstructor the module descriptor constructor to put the copy inside */ protected final <T extends ModuleDescriptor> T copy0(@NotNull Supplier<T> concreteConstructor) { T descriptorCopy = concreteConstructor.get(); descriptorCopy.setId(getId()); descriptorCopy.setNamespace(getNamespace()); descriptorCopy.setTimestamp(getTimestamp()); descriptorCopy.setModuleVersion(getModuleVersion()); Copyable.deepCopy(getModelRootDescriptors(), descriptorCopy.getModelRootDescriptors()); Copyable.deepCopy(getModuleFacetDescriptors(), descriptorCopy.getModuleFacetDescriptors()); Copyable.deepCopy(getDependencies(), descriptorCopy.getDependencies()); descriptorCopy.getUsedLanguages().addAll(getUsedLanguages()); descriptorCopy.getUsedDevkits().addAll(getUsedDevkits()); descriptorCopy.getLanguageVersions().putAll(getLanguageVersions()); descriptorCopy.getDependencyVersions().putAll(getDependencyVersions()); descriptorCopy.getAdditionalJavaStubPaths().addAll(getAdditionalJavaStubPaths()); descriptorCopy.getSourcePaths().addAll(getSourcePaths()); copyDeploymentDescriptor(descriptorCopy); descriptorCopy.setLoadException(getLoadException()); descriptorCopy.setUseTransientOutput(isUseTransientOutput()); descriptorCopy.setHasDependencyVersions(hasDependencyVersions()); descriptorCopy.setHasLanguageVersions(hasLanguageVersions()); return descriptorCopy; } private <T extends ModuleDescriptor> void copyDeploymentDescriptor(T descriptorCopy) { DeploymentDescriptor deploymentDescriptor = getDeploymentDescriptor(); if (deploymentDescriptor != null) { deploymentDescriptor = deploymentDescriptor.copy(); } descriptorCopy.setDeploymentDescriptor(deploymentDescriptor); } @NotNull @Override public ModuleDescriptor copy() { return copy0(ModuleDescriptor::new); } }