/* * 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.library; import jetbrains.mps.generator.fileGenerator.FileGenerationUtil; import jetbrains.mps.project.ProjectPathUtil; import jetbrains.mps.project.io.DescriptorIO; import jetbrains.mps.project.io.DescriptorIOException; import jetbrains.mps.project.io.DescriptorIOFacade; import jetbrains.mps.project.persistence.DeploymentDescriptorPersistence; import jetbrains.mps.project.persistence.ModuleReadException; import jetbrains.mps.project.structure.modules.DeploymentDescriptor; import jetbrains.mps.project.structure.modules.DevkitDescriptor; import jetbrains.mps.project.structure.modules.GeneratorDescriptor; import jetbrains.mps.project.structure.modules.LanguageDescriptor; import jetbrains.mps.project.structure.modules.LibraryDescriptor; import jetbrains.mps.project.structure.modules.ModuleDescriptor; import jetbrains.mps.project.structure.modules.SolutionDescriptor; import jetbrains.mps.util.PathManager; import jetbrains.mps.util.annotation.ToRemove; import jetbrains.mps.util.io.ModelInputStream; import jetbrains.mps.util.io.ModelOutputStream; import jetbrains.mps.vfs.FileRefresh; import jetbrains.mps.vfs.FileSystem; import jetbrains.mps.vfs.IFile; import jetbrains.mps.vfs.IFileUtils; import jetbrains.mps.vfs.impl.JarEntryFile; import jetbrains.mps.vfs.path.Path; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.annotations.Immutable; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Set; /** * Detects modules in a folder. * Methods of this class are not thread-safe, do not share instances of this class between threads. * * NB: we will go inside the jar if it either has a 'modules' folder (with modules (!)) or has a module.xml file in the META-INF folder */ public final class ModulesMiner { private static final Logger LOG = LogManager.getLogger(ModulesMiner.class); private static final String DOT_JAR = JarEntryFile.DOT_JAR; public static final String META_INF = "META-INF"; private static final String JAR_SEPARATOR = Path.ARCHIVE_SEPARATOR; public static final String MODULE_XML = "module.xml"; public static final String MODULES_DIR = "modules"; public static final String META_INF_MODULE_XML = META_INF + "/" + MODULE_XML; // deployment descriptor resides at abc-lang.jar!/META-INF/module.xml public static final String SLASH_META_INF_MODULE_XML = JAR_SEPARATOR + META_INF_MODULE_XML; private static final String SOURCES_MODULE_DIR = "module"; // source descriptor (if packaged) resides at the abc-lang-src.jar!/module/abc-lang.msd // excludes is going to be updated from #processExcludes, ensure it can be changed. private final Set<IFile> myExcludes = new HashSet<IFile>(); private final List<ModuleHandle> myOutcome = new ArrayList<ModuleHandle>(); public ModulesMiner() { this(Collections.emptySet()); } public ModulesMiner(@NotNull Collection<IFile> excludes) { myExcludes.addAll(excludes); } /** * Updates {@link #getCollectedModules() outcome} and excludes, may be invoked several times. * @param file folder or file (descriptor or jar) to look for modules at. * @return {@code this} for convenience (chained calls) */ @NotNull public ModulesMiner collectModules(IFile file) { LOG.debug("Reading modules from " + file); if (!needProcess(file)) { return this; } if (file.isDirectory()) { readModuleDescriptorsFromFolder(file); } else { if (IFileUtils.isJarFile(file)) { readModuleDescriptorsFromJarFile(file); } else { trySourceModuleDescriptorsFromFile(file); } } return this; } @NotNull public Collection<ModuleHandle> getCollectedModules() { ArrayList<ModuleHandle> rv = new ArrayList<>(myOutcome); // sort values so that languages come in front of generators. rv.sort(Comparator.comparingInt(v -> v.getDescriptor() instanceof LanguageDescriptor ? 0 : 1)); return Collections.unmodifiableList(rv); } @Deprecated @ToRemove(version = 3.3) public List<ModuleHandle> collectModules(IFile dir, boolean refreshFiles) { return collectModules(dir, null, refreshFiles); } /** * @deprecated use {@link #collectModules(IFile)} instead. * To refresh files, use {@link FileRefresh} directly */ @Deprecated @ToRemove(version = 3.3) public List<ModuleHandle> collectModules(IFile dir, Set<IFile> excludes, boolean refreshFiles) { if (excludes != null) { myExcludes.addAll(excludes); } if (refreshFiles) { LOG.debug("Refreshing recursively in the " + dir); new FileRefresh(dir).run(); } myOutcome.clear(); collectModules(dir); return new ArrayList<>(myOutcome); } private boolean needProcess(IFile file) { return !getFileSystem().isFileIgnored(file.getName()) && !myExcludes.contains(file); } private boolean trySourceModuleDescriptorsFromFile(IFile file) { assert !file.isDirectory(); if (isSourceModuleFile(file)) { ModuleDescriptor moduleDescriptor = loadSourceModuleDescriptor(file); if (moduleDescriptor != null) { processExcludes(file, moduleDescriptor); fillOutcome(new ModuleHandle(file, moduleDescriptor), true); return true; // unlike other tryXXX methods, here we make sure descriptor actually read // because of .iml files (see DescriptorIOFacade) that are treated as solution module and thus break // readModuleDescriptorsFromFolder assumption of a single descriptor per dir. } } return false; } /** * Looks for source module descriptors in the given folder, and if none found, tries * deployment descriptor, collection of modules (under modules/) and deployed jars. * Dives into nested folders unless the folder is home for unjarred deployed module or modules/ collection */ private void readModuleDescriptorsFromFolder(IFile folder) { assert folder.isDirectory(); if (!needProcess(folder)) { // files and folders are collected prior to processing of descriptor excludes, // chances are we get here in a recursive call with a folder marked to exclude. return; } ArrayList<IFile> files = new ArrayList<>(); ArrayList<IFile> folders = new ArrayList<>(); for (IFile f : folder.getChildren()) { if (!needProcess(f)) { continue; } if (f.isDirectory()) { folders.add(f); } else { files.add(f); } }; boolean sourceModuleFound = false; for (IFile f : files) { if (trySourceModuleDescriptorsFromFile(f)) { sourceModuleFound = true; // XXX Generally, I shall not expect more than 1 module descriptor per directory, and shall break loop here. // However, it's not true for e.g. devkits/, where few devkit descriptors reside } } // code below intentionally uses explicit !sourceModuleFound check 2 times, to illustrate // independent logical blocks, I'm not yet sure which gonna stay if (!sourceModuleFound) { // don't expect nested module collections or deployed modules nested into another module // // folder/modules/module-folder-x if (tryReadFromModulesDir(folder, folder.getDescendant(MODULES_DIR))) { // no need to process nested jars or folders return; } // folder/META-INF/module.xml if (tryModuleFromDeploymentDescriptor(folder, folder.getDescendant(META_INF).getDescendant(MODULE_XML))) { // no need to process nested jars or folders return; } } if (!sourceModuleFound) { // nor expect jar with modules nested into another module, and // do not look into jars under a folder with either META-INF/ or modules/ they are likely auxiliary. for (IFile f : files) { if (IFileUtils.isJarFile(f)) { readModuleDescriptorsFromJarFile(f); } } } // It's possible to have extra module under module-folder, i.e. module-folder/module2-folder/descriptor-file2, // e.g. baseLanguage/bl.mpl and baseLanguage/solutions/ folders.forEach(this::readModuleDescriptorsFromFolder); } /** * There are 2 scenarios we consider here: * Layout 1: * folder/module.name.jar * META-INF/module.xml * whatever file|folder structure of the module * Layout2: * folder/group.name.jar * modules/ * module1.name/descriptor1.file * module2.name/descriptor2.file * ... * whatever file|folder structure (e.g. classes, resources) for the modules listed * * Note, we don't walk arbitrary jars (i.e. we ignore -src.jar and -generator.jar because there's neither META-INF/module.xml nor modules/). * * @param jarFile {@code folder/module.name.jar} from the sample layouts above. */ private void readModuleDescriptorsFromJarFile(IFile jarFile) { assert IFileUtils.isJarFile(jarFile); IFile jarFileRoot = IFileUtils.stepIntoJar(jarFile); if (tryModuleFromDeploymentDescriptor(jarFile, jarFileRoot.getDescendant(META_INF).getDescendant(MODULE_XML))) { return; } tryReadFromModulesDir(jarFile, jarFileRoot.getDescendant(MODULES_DIR)); } /** * Attempt to read a module with a layout: * moduleHome/ * META-INF/module.xml * whatever file|folder structure of the module * * @param moduleHome either a jar file or a directory, base location for any module-relative paths * @param moduleXml path to META-INF/module.xml * @return true if module found under the {@code moduleHome} */ private boolean tryModuleFromDeploymentDescriptor(IFile moduleHome, IFile moduleXml) { if (moduleXml.exists() && !moduleXml.isDirectory()) { ModuleDescriptor moduleDescriptor = loadDeploymentDescriptor(moduleHome, moduleXml); if (moduleDescriptor != null) { processExcludes(moduleXml, moduleDescriptor); // do I really need to exclude anything for DD? There's source module, indeed. fillOutcome(new ModuleHandle(moduleXml, moduleDescriptor), false); } // even if we didn't succeed to read a module, presence of META-INF/module.xml prevents processing of any other possible // module location under moduleHome return true; } return false; } /** * Layout with collection of modules under single deployment element (jar or folder): * * bundleHome/ * whatever file|folder structure (e.g. classes, resources) for the modules listed, accessible relative to bundleHome * modules/ * module1.name/module1.descriptor.msd * module2.name/module2.descriptor.mpl * ... * * Note, at the moment, we don't expect nested collections or deployed modules under modules/ * * @param bundleHome root location for collection of modules (jar or a directory) * @return {@code true} if module collection found under bundle home */ private boolean tryReadFromModulesDir(IFile bundleHome, IFile modulesDir) { if (modulesDir.exists() && modulesDir.isDirectory()) { for (IFile child : modulesDir.getChildren()) { if (child.isDirectory()) { // perhaps, we could allow nested directories in tryReadModuleDescriptor, but at the moment // we expect 1 level of directories only (XXX what about mps/testbench/modules/aaa.test/languages - disjunction of RVs would help). tryReadModuleDescriptor(bundleHome, child); // XXX may collect folders without modules and dig into them, with e.g. readModuleDescriptorsFromFolder(), just need to pass bundleHome there } // expect no descriptors under modules/ } return true; // disjunction of tryReadModuleDescriptor return values, perhaps? } return false; } /** * Read descriptor for a module bundled under bundleHome/.../moduleHomeDir, if any. * @return {@code true} if module descriptor found under bundle home */ private boolean tryReadModuleDescriptor(IFile bundleHome, IFile moduleHomeDir) { assert moduleHomeDir.isDirectory(); for (IFile child : moduleHomeDir.getChildren()) { if (child.isDirectory()) { continue; } // XXX now we ignore deployment descriptors here, is it desired? if (trySourceModuleDescriptorsFromFile(child)) { return true; } } return false; } /** * Attempts to load module descriptor from file. * Updates {@link #getCollectedModules()} collection. * NOTE: single file could trigger more than one module loaded (e.g. lang.mpl loads generators), returned value * is the 'primary' module, i.e. language. Other loaded modules are available in {@link #getCollectedModules()} * * Note, loading a file with language module not necessarily triggers loading of respective generators, only language source * module would pick generator modules up. Deployed language doesn't load generators. * * Please do not use this method, it gonna fade away. With few modules coming from the same file, its API is not handy. * * @param file descriptor file to parse for module information * @return handle for descriptor loaded from file */ @NotNull public ModuleHandle loadModuleHandle(@NotNull IFile file) { final ModuleHandle moduleHandle = new ModuleHandle(file, loadModuleDescriptor(file)); fillOutcome(moduleHandle, !file.getPath().endsWith(SLASH_META_INF_MODULE_XML)); return moduleHandle; } private void fillOutcome(ModuleHandle moduleHandle, boolean isSourceNotDeployment) { myOutcome.add(moduleHandle); // Deployed Language and Generator modules have their own DD now, and their modules either listed (almost) directly, in GenerateTask (till the moment // build language does this in the proper way for <generate> task), or SLibrary(dir) gives them when walks languages/ fs location. // The only case when we need to extract generators out from language's MD is when we walk non-deployed module sources. if (isSourceNotDeployment && moduleHandle.getDescriptor() instanceof LanguageDescriptor) { for (GeneratorDescriptor gd : ((LanguageDescriptor) moduleHandle.getDescriptor()).getGenerators()) { myOutcome.add(new ModuleHandle(moduleHandle.getFile(), gd)); } } } /** * read a module file and update excludes set with output locations (classes, generated sources) of the module * if file points to deployment descriptor, attempt to read descriptor of source module, associates DD with it and return it, or DD if no source * module found. */ @Nullable private ModuleDescriptor loadModuleDescriptor(IFile file) { String filePath = file.getPath(); ModuleDescriptor descriptor; if (filePath.endsWith(SLASH_META_INF_MODULE_XML)) { IFile moduleHome; if (file.isInArchive()) { moduleHome = file.getBundleHome(); } else { // IFile.getBundleHome is not smart enough to recognize META-INF/module.xml in a regular directory (not archive), and yields // wrong result then (file.getParent() == META-INF/ location which won't help to locate libraries). // Instead, assume META-INF/module.xml is at the root of a module location (which if generally the case). moduleHome = file.getParent().getParent(); } // there are no excludes in deployment descriptor, but loadDD reads and returns MD from source module, if any. // OTOH, file is the one under META-INF, and no chances to find neither source_gen nor test_gen relative to it // (need one at lang-src.jar!/module/source.lang.mpl) descriptor = loadDeploymentDescriptor(moduleHome, file); } else { descriptor = loadSourceModuleDescriptor(file); } if (descriptor != null) { processExcludes(file, descriptor); } return descriptor; } private ModuleDescriptor loadSourceModuleDescriptor(IFile file) { try { DescriptorIO<? extends ModuleDescriptor> descriptorIO = DescriptorIOFacade.getInstance().fromFileType(file); return descriptorIO.readFromFile(file); } catch (DescriptorIOException t) { LOG.error("Fail to load module from descriptor " + file.getPath(), t); return null; } catch (Exception e) { LOG.error("Unknown error while trying to load source module descriptor " + file.getPath(), e); return null; } } /** * loads deployment descriptor and try to load the corresponding source module descriptor * Both arguments are != null. * @param moduleHome either a jar file or a directory, base location for any module-relative paths * @param file META-INF/module.xml */ private ModuleDescriptor loadDeploymentDescriptor(IFile moduleHome, IFile file) { try { DeploymentDescriptor deploymentDescriptor = DeploymentDescriptorPersistence.loadDeploymentDescriptor(file); ModuleDescriptor result = null; IFile sourceDescriptorFile = getSourceDescriptorFile(file, deploymentDescriptor); if (sourceDescriptorFile != null) { result = loadSourceModuleDescriptor(sourceDescriptorFile); if (DeploymentDescriptor.TYPE_GENERATOR.equals(deploymentDescriptor.getType()) && result instanceof LanguageDescriptor) { // source module keeps generators as part of a language (the only possible layout for the time being) for (GeneratorDescriptor gd : ((LanguageDescriptor) result).getGenerators()) { if (gd.getId().equals(deploymentDescriptor.getId())) { result = gd; break; } } if (false == result instanceof GeneratorDescriptor) { // it's wrong to have DD for generator with source MD of a Language, better not to have any result = null; } } } for (ListIterator<String> it = deploymentDescriptor.getClasspath().listIterator(); it.hasNext(); ) { // Not sure it's the best idea to change paths inplace, but at the moment it's the only place I'm aware of moduleHome // Source module descriptors resolve paths during read using MacroHelper, why not the same here with DD? // Alternatively, could keep moduleHome value within DD and resolve on use String cpEntry = it.next(); if (".".equals(cpEntry)) { it.set(moduleHome.getPath()); } else if (!cpEntry.isEmpty()) { StringBuilder moduleHomePath = new StringBuilder(); if (IFileUtils.isJarFile(moduleHome)) { moduleHomePath.append(IFileUtils.stepIntoJar(moduleHome).getPath()); } else { moduleHomePath.append(moduleHome.getPath()); } if (cpEntry.charAt(0) != '/') { // it doesn't hurt to have extra fs delimiter, e.g. if moduleHome doesn't ends with one. moduleHomePath.append('/'); } moduleHomePath.append(cpEntry); it.set(moduleHome.getFileSystem().getFile(moduleHomePath.toString()).getPath()); } } // TODO create module without sources if (result != null) { result.setDeploymentDescriptor(deploymentDescriptor); // fix stubs libraries: // META-INF/module.xml contains info about model libs, while clients generally look at MD.getAdditionalJavaStubPaths() which were not // updated by build language at deployment time and still points to design-time lib location. // Here we ignore stub libraries from source module descriptor, use libs from DeploymentDescriptor result.getAdditionalJavaStubPaths().clear(); // XXX I don't like this assumption that libraries are siblings to module home, but have no better idea now. IFile bundleParent = moduleHome.getParent(); for (String jarFile : deploymentDescriptor.getLibraries()) { IFile jar = jarFile.startsWith("/") ? bundleParent.getFileSystem().getFile(PathManager.getHomePath() + jarFile) : bundleParent.getDescendant(jarFile); if (jar.exists()) { String path = jar.getPath(); result.getAdditionalJavaStubPaths().add(path); } } } // XXX why don't we return DD if no source MD found? return result; } catch (ModuleReadException e) { LOG.error("Exception while loading a deployment descriptor from the path " + file.getPath(), e); return null; } catch (Exception e) { LOG.error("Unknown error while loading a deployment descriptor from the path " + file.getPath(), e); return null; } } // part of processExcludes() with common code for any module type private void processModuleExcludes(jetbrains.mps.vfs.openapi.FileSystem fileSystem, ModuleDescriptor descriptor) { String generatorOutputPath = ProjectPathUtil.getGeneratorOutputPath(descriptor); if (generatorOutputPath != null) { IFile genOutputFile = fileSystem.getFile(generatorOutputPath); excludeGeneratedSourcesDir(genOutputFile); // we don't care if there's indeed tests facet or if the folder exists // and I don't see why TestsFacetImpl.fromModuleDescriptor(arbitraryFile, MD) gives better result than hard-coded, source_gen-relative path, // namely why magic with possiblyDeploymentDescriptorFile.getParent().getDescendant is better than the assumption test_gen // is at the same level as source_gen // Proper solution would be to use default {module}/test_gen in ModuleDeploymentPersistence for Tests Facet, let it resolve to FS location // and use the value here much like we did for getGeneratorOutputPath(MD) above. excludeGeneratedSourcesDir(genOutputFile.getParent().getDescendant("test_gen")); // // excludeIdeaClassesGen(descriptorFile, descriptor); // Again, no reason to expect ProjectPathUtil.getClassesFolder(arbitraryFile) to yield any more meaningful result than 'sibling classes/'. myExcludes.add(genOutputFile.getParent().getDescendant("classes")); // // excludeClassesGen(descriptorFile, descriptor); // Yet one more, ProjectPathUtil.getClassesGenFolder(descriptorFile, descriptor instanceof GeneratorDescriptor) replaced with // 'sibling classes_gen/' as ProjectPathUtil.getGeneratorOutputPath(descriptor) (together with MDPersistence code) gives us proper FS location // of generator's source_gen, and no reason for md.IsInstanceOf(GeneratorMD) -> getDescendant("generator") magic // XXX Would be great to reuse constant from JavaModuleFacetImpl#getClassesGen myExcludes.add(genOutputFile.getParent().getDescendant("classes_gen")); // and as for jars (-src.jar and -generator.jar) that used to be excluded if descriptorFile.isReadOnly(), // check readModuleDescriptorsFromFolder(IFile) - it reads jar only if there's META-INF/module.xml or modules/, neither of this happens to // -generator.jar nor -src.jar, so no reason to put their roots into excludes (on a side note, why not ".jar" itself, but ".jar!/"?) } for (String p : descriptor.getSourcePaths()) { myExcludes.add(fileSystem.getFile(p)); } for (String entry : descriptor.getAdditionalJavaStubPaths()) { myExcludes.add(fileSystem.getFile(entry)); } } // makes sense for module descriptors from loadSourceModuleDescriptor(), not for DeploymentDescriptor private void processExcludes(@NotNull IFile descriptorFile, ModuleDescriptor descriptor) { // in fact, descriptorFile.isReadOnly doesn't really mean there could be no dirs to exclude // perhaps, there should be two distinct miners, one to look up source modules, and another one for deployed? if (descriptor == null || descriptorFile.isReadOnly()) { return; } jetbrains.mps.vfs.openapi.FileSystem fileSystem = descriptorFile.getFileSystem(); processModuleExcludes(fileSystem, descriptor); if (descriptor instanceof LanguageDescriptor) { for (GeneratorDescriptor generator : ((LanguageDescriptor) descriptor).getGenerators()) { processModuleExcludes(fileSystem, generator); } } } private void excludeGeneratedSourcesDir(IFile sourceDir) { if (sourceDir != null) { myExcludes.add(sourceDir); // todo: why? if (!sourceDir.isReadOnly()) { myExcludes.add(FileGenerationUtil.getCachesDir(sourceDir)); } } } static boolean isSourceModuleFile(IFile file) { return !file.isDirectory() && DescriptorIOFacade.getInstance().fromFileType(file) != null; } /** * @param deploymentFile -- the path to deployment descriptor, expected to reside in a jar, and end with {@link #SLASH_META_INF_MODULE_XML} */ @Nullable public static IFile getSourceDescriptorFile(@NotNull IFile deploymentFile, @NotNull DeploymentDescriptor deploymentDescriptor) { String sourcesJarPath = deploymentDescriptor.getSourcesJar(); if (sourcesJarPath == null || deploymentDescriptor.getDescriptorFile() == null) { return null; } // modules without compiled sources get single jar, packaged similar to regular -src.jar, with addition of META-INF/module.xml // To avoid major refactoring of MM, module content left under module/ as in -src.jar, so here we just need to notice there's no // other jar, and process with original/source descriptor from the same as the one of META-INF/module.xml. if (sourcesJarPath.isEmpty() || sourcesJarPath.equals(".")) { // META-INF/module.xml/../../ return deploymentFile.getParent().getParent().getDescendant(SOURCES_MODULE_DIR).getDescendant(deploymentDescriptor.getDescriptorFile()); } else { // FIXME any idea why the code below mangles path instead of going up/down with regular FS getParent/getDescendant operations? // I suspect it's just incomplete refactoring in 4c5b44bc9e1242d4e4399dc816e5caa01855dc00, right? jetbrains.mps.vfs.openapi.FileSystem fileSystem = deploymentFile.getFileSystem(); String deploymentPath = deploymentFile.getPath(); String moduleJarPath = deploymentPath.substring(0, deploymentPath.length() - SLASH_META_INF_MODULE_XML.length()); IFile moduleJar = fileSystem.getFile(moduleJarPath); IFile sourcesJar = moduleJar.getParent().getDescendant(deploymentDescriptor.getSourcesJar()); if (sourcesJar.exists()) { return fileSystem.getFile(sourcesJar.getPath() + JAR_SEPARATOR + SOURCES_MODULE_DIR + "/" + deploymentDescriptor.getDescriptorFile()); } return null; } } @NotNull private static FileSystem getFileSystem() { return FileSystem.getInstance(); } public void saveHandle(@NotNull ModuleHandle handle, ModelOutputStream stream) throws IOException { stream.writeShort(0x1be0); stream.writeString(handle.getFile().getPath()); ModuleDescriptor descriptor = handle.getDescriptor(); if (descriptor instanceof LanguageDescriptor) { stream.writeByte(1); } else if (descriptor instanceof SolutionDescriptor) { stream.writeByte(2); } else if (descriptor instanceof DevkitDescriptor) { stream.writeByte(3); } else if (descriptor instanceof GeneratorDescriptor) { stream.writeByte(4); } else if (descriptor instanceof LibraryDescriptor) { stream.writeByte(5); } else if (descriptor instanceof DeploymentDescriptor) { stream.writeByte(6); } else { throw new IllegalArgumentException("unknown module!"); } descriptor.save(stream); } public ModuleHandle loadHandle(ModelInputStream stream) throws IOException { if (stream.readShort() != 0x1be0) throw new IOException("bad stream: no start marker"); String file = stream.readString(); ModuleDescriptor descriptor; int type = stream.readByte(); if (type == 1) { descriptor = new LanguageDescriptor(); } else if (type == 2) { descriptor = new SolutionDescriptor(); } else if (type == 3) { descriptor = new DevkitDescriptor(); } else if (type == 4) { descriptor = new GeneratorDescriptor(); } else if (type == 5) { descriptor = new LibraryDescriptor(); } else if (type == 6) { descriptor = new DeploymentDescriptor(); } else { throw new IOException("broken stream: invalid descriptor type"); } descriptor.load(stream); return new ModuleHandle(getFileSystem().getFile(file), descriptor); } @Immutable public static final class ModuleHandle { private final IFile myFile; private final ModuleDescriptor myDescriptor; public ModuleHandle(@NotNull IFile file, @Nullable ModuleDescriptor descriptor) { myFile = file; myDescriptor = descriptor; } @NotNull public IFile getFile() { return myFile; } @Nullable public ModuleDescriptor getDescriptor() { return myDescriptor; } @Override public String toString() { return myDescriptor == null ? "[null descriptor]" : myDescriptor.getNamespace(); } } }