/*
* 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();
}
}
}